文章目录
- TextCNN
- 1.论文结构
- 2.TextCNN 结构
- 模型的正则化
- 3.实验结果与分析
- 4.论文总结
- 5.代码实现
TextCNN
1.论文结构
2.TextCNN 结构
TextCNN的结果不复杂,第一层是输入层把原始的词转换为向量表示,然后接下来是卷积提取不同层次的特征,并用的是Rule激活函数,池化层对卷积层的结果做Max pooling, 最后经过全连接和softmax进行输出得到句子的类别。
具体的参数设置参考文献《A Sensitivity Analysis of (and Practitioners’ Guide to) Convolutional Neural Networks for Sentence Classificatio》
多通道结构
这里提到了,词向量模型用了两个channels, 一个是静态, 一个是动态。
- 静态的意思是在反向传播到词向量这里, 词向量不变,词向量不变意味着不进行反向传播。
- 动态的意思是在反向传播到词向量这里,词向量微调,词向量微调意味着进行反向传播。
模型的正则化
- Dropout:抓爆
在神经网络的传播过程中,让某个神经元以一定的概率p停止工作,从而增加模型的泛化能力
- L2-正则
3.实验结果与分析
MR:电影评论极性判断数据集
SST-1:斯坦福情感分类标准数据集(5类:(very positive, positive, neutral, negative, very negative)
SST-2:斯坦福情感二分类数据集
Subj:主客观判断数据集
TREC:TREC问题类型数据集
CR:商品(cameras, MP3s etc.)评价极性判断数据集
MPQA:MPQA的观点极性判断数据集
下表中,c代表分类数量
l应该是句子的最大长度
N是数据集大小
V是词表大小
Vpre是在预训练词表中出现的单词数量
test表示是否划分验证集,如果没有划分则采用CV(cross valid交叉验证的方式划分)
4.论文总结
关键点
·预训练的词向量—-Word2Vec、Glove
·卷积神经网络结构——一维卷积、池化层
·超参选择——卷积核选择、词向量方式选择
创新点
·提出了基于CNN的文本分类模型TextCNN
·提出了多种词向量设置方式
·在四个文本分类任务上取得最优的结果
·对超参进行大量实验和分析
开创性地使用神经网络结合预训练的词向量来解决文本分类问题
进行充分的实验分析,探讨词向量对模型性能的影响
探讨神经网络中的一些操作对当前任务的影响
5.代码实现
准备工作
项目环境配置
·Python3.5
·jupyter notebook
·torch 1.4.0
·numpy 1.16.2
·gensim 3.8.1用来读取预训练词向量
·torchsummary 1.5.1
数据集下载
MR数据集https:///www.cs.cornell.edu/people/pabo/movie-review-data/
预训练词向量word2vec下载:https:/pan.baidu.com/s/1jJ9eAaE
或者:http://pan.baidu.com/s/1kTCQqft
glove下载http://downloads.cs.stanford.edu/nlp/data/glove.840B.300d.zip
数据处理
# coding:utf-8
from torch.utils import data
import os
import random
import numpy as np
import nltk
import torch
from gensim.test.utils import datapath, get_tmpfile
from gensim.models import KeyedVectors
# 总体步骤:
# ·词向量导入
# ·数据集加载
# ·构建word2id并pad成相同长度
# ·求词向量均值和方差
# ·生成词向量
# ·生成训练集、验证集和测试集
# 继承自torch的Dataset,要实现:
# __init__
# __getitem__
# __len__
class MR_Dataset(data.Dataset):
def __init__(self, state="train", k=0, embedding_type="word2vec"):
self.path = os.path.abspath('.')
if "data" not in self.path:
self.path += "/data"
# 数据集加载,每行都是一句话
pos_samples = open(self.path + "/MR/rt-polarity.pos", errors="ignore").readlines()
neg_samples = open(self.path + "/MR/rt-polarity.neg", errors="ignore").readlines()
# 把正负样本放在一块
datas = pos_samples + neg_samples
# datas = [nltk.word_tokenize(data) for data in datas]
# 用空格进行分词
datas = [data.split() for data in datas]
# 求句子最大长度,将所有句子pad成一样的长度;
# 有时也可以pad为平均长度,如果使用平均长度,对于超过长度的句子需要截断。
max_sample_length = max([len(sample) for sample in datas])
# 为正负样本设置标签
labels = [1] * len(pos_samples) + [0] * len(neg_samples)
word2id = {"<pad>": 0} # 生成word2id,pad对应0
for i, data in enumerate(datas):
for j, word in enumerate(data):
# 词不还没加入word2id中则加入,并设置其id
if word2id.get(word) is None:
word2id[word] = len(word2id)
# 设置每个句子中所有词,替换为ID
datas[i][j] = word2id[word]
# 将句子按最大长度进行pad
datas[i] = datas[i] + [0] * (max_sample_length - len(datas[i]))
# 如果是按平均长度则按下面语句进行截断或补齐,max_sample_length代表平均长度
# datas[i] = datas[i][0:max_sample_length]+[0]*(max_sample_length-len(datas[i]))
self.n_vocab = len(word2id)
self.word2id = word2id
#根据配置取不同的预训练词向量
if embedding_type == "word2vec":
self.get_word2vec()
elif embedding_type == "glove":
self.get_glove_embedding()
else:
pass
# self.get_word2vec()
# 由于训练集中的数据前半部分是正样本,后半部分是负样本,需要打乱训练集
# 把数据和标签放到一起打乱
c = list(zip(datas, labels)) # 打乱训练集
random.seed(1)
random.shuffle(c)
# 再把数据和标签分开
datas[:], labels[:] = zip(*c)
# 生成训练集、验证集和测试集
# 总的数据分成10份,第k份作为测试集,其他9份再分
# 其他9分的后10%做为验证集,前90%做为训练集
if state == "train": # 生成训练集
# 取除第k份外其他9份
self.datas = datas[:int(k * len(datas) / 10)] + datas[int((k + 1) * len(datas) / 10):]
self.labels = labels[:int(k * len(datas) / 10)] + labels[int((k + 1) * len(labels) / 10):]
# 取前90%做为训练集
self.datas = np.array(self.datas[0:int(0.9 * len(self.datas))])
self.labels = np.array(self.labels[0:int(0.9 * len(self.labels))])
elif state == "valid": # 生成验证集
# 取除第k份外其他9份
self.datas = datas[:int(k * len(datas) / 10)] + datas[int((k + 1) * len(datas) / 10):]
self.labels = labels[:int(k * len(datas) / 10)] + labels[int((k + 1) * len(labels) / 10):]
# 取后10%做为验证集
self.datas = np.array(self.datas[int(0.9 * len(self.datas)):])
self.labels = np.array(self.labels[int(0.9 * len(self.labels)):])
elif state == "test": # 生成测试集
# 第k份作为测试集
self.datas = np.array(datas[int(k * len(datas) / 10):int((k + 1) * len(datas) / 10)])
self.labels = np.array(labels[int(k * len(datas) / 10):int((k + 1) * len(datas) / 10)])
def __getitem__(self, index):
return self.datas[index], self.labels[index]
def __len__(self):
return len(self.datas)
def get_glove_embedding(self):
'''
生成glove词向量
:return: 根据词表生成词向量
'''
if not os.path.exists(self.path + "/glove_embedding_mr.npy"): # 如果已经保存了词向量,就直接读取
# 与word2vec不一样的是glove文件是txt格式,要先转换为word2vec格式
# 这个转换过程比较慢,所以转换好就先保存,下次直接读。
if not os.path.exists(self.path + "/test_word2vec.txt"):
glove_file = datapath(self.path + '/glove.840B.300d.txt')
# 指定转化为word2vec格式后文件的位置
tmp_file = get_tmpfile(self.path + "/glove_word2vec.txt")
from gensim.scripts.glove2word2vec import glove2word2vec
glove2word2vec(glove_file, tmp_file)
else:
tmp_file = get_tmpfile(self.path + "/glove_word2vec.txt")
print("Reading Glove Embedding...")
# 注意这里的binary=True不用写。
wvmodel = KeyedVectors.load_word2vec_format(tmp_file)
# 求词向量均值和方差
# 论文中提到,用方差对未知词进行初始化对于训练词向量的效果很不错
tmp = []
for word, index in self.word2id.items():
try:
tmp.append(wvmodel.get_vector(word))
except:
pass
mean = np.mean(np.array(tmp))
std = np.std(np.array(tmp))
print(mean, std)
# 用上面的词向量均值和方差来生成词向量
vocab_size = self.n_vocab
embed_size = 300
embedding_weights = np.random.normal(mean, std, [vocab_size, embed_size]) # 正态分布初始化方法
for word, index in self.word2id.items():
try:
# 如果预训练词向量中有对应的词就使用预训练的词向量,否则就用正态分布初始化的词向量
embedding_weights[index, :] = wvmodel.get_vector(word)
except:
pass
# 由于每次读取这个东西很费时,所以处理好后保存下来,下次直接读取
np.save(self.path + "/glove_embedding_mr.npy", embedding_weights) # 保存生成的词向量
else:
embedding_weights = np.load(self.path + "/glove_embedding_mr.npy") # 载入生成的词向量
self.weight = embedding_weights
def get_word2vec(self):
'''
生成word2vec词向量
:return: 根据词表生成的词向量
'''
if not os.path.exists(self.path + "/word2vec_embedding_mr.npy"): # 如果已经保存了词向量,就直接读取
print("Reading word2vec Embedding...")
# 加载预训练的Word2Vec词向量
wvmodel = KeyedVectors.load_word2vec_format(self.path + "/GoogleNews-vectors-negative300.bin.gz",
binary=True)
tmp = []
for word, index in self.word2id.items():
try:
tmp.append(wvmodel.get_vector(word))
except:
pass
mean = np.mean(np.array(tmp))
std = np.std(np.array(tmp))
print(mean, std)
vocab_size = self.n_vocab
embed_size = 300
embedding_weights = np.random.normal(mean, std, [vocab_size, embed_size]) # 正太分布初始化方法
for word, index in self.word2id.items():
try:
embedding_weights[index, :] = wvmodel.get_vector(word)
except:
pass
np.save(self.path + "/word2vec_embedding_mr.npy", embedding_weights) # 保存生成的词向量
else:
embedding_weights = np.load(self.path + "/word2vec_embedding_mr.npy") # 载入生成的词向量
self.weight = embedding_weights
if __name__ == "__main__":
mr_train_dataset = MR_Dataset()
print(mr_train_dataset.__len__())
print(mr_train_dataset[0])
mr_valid_dataset = MR_Dataset("valid")
print(mr_valid_dataset.__len__())
print(mr_valid_dataset[0])
mr_test_dataset = MR_Dataset("test")
print(mr_test_dataset.__len__())
print(mr_test_dataset[0])
模型的构建
# -*- coding: utf-8 -*-
import torch
import torch.autograd as autograd
import torch.nn as nn
import torch.nn.functional as F
from .BasicModule import BasicModule
class TextCNN(BasicModule):
def __init__(self, config):
super(TextCNN, self).__init__()
# 嵌入层
if config.embedding_pretrained is not None:
self.embedding = nn.Embedding.from_pretrained(config.embedding_pretrained, freeze=False)
else:
self.embedding = nn.Embedding(config.n_vocab, config.embed_size)
# 卷积层,按config中设置卷积核数量进行设置
if config.cuda:
self.convs = [nn.Conv1d(config.embed_size, config.filter_num, filter_size).cuda()
for filter_size in config.filters]
else:
self.convs = [nn.Conv1d(config.embed_size, config.filter_num, filter_size)
for filter_size in config.filters]
# Dropout层
self.dropout = nn.Dropout(config.dropout)
# 分类层
self.fc = nn.Linear(config.filter_num*len(config.filters), config.label_num)
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):
out = self.embedding(x) # batch_size*length*embedding_size
# 这里把第1个维度和第2个维度交换一下(起始维度编号是0)
out = out.transpose(1, 2).contiguous() # batch_size*embedding_size*length
# 这里对所有卷积核做卷积的结果(这里卷积和pooling一起做:conv_and_pool)进行循环
out = torch.cat([self.conv_and_pool(out, conv) for conv in self.convs], 1) # batch_size*(filter_num*len(filters))
out = self.dropout(out)
out = self.fc(out) # batch_size*label_num
return out
if __name__ == '__main__':
print('running the TextCNN...')
有卷积核做卷积的结果(这里卷积和pooling一起做:conv_and_pool)进行循环
out = torch.cat([self.conv_and_pool(out, conv) for conv in self.convs], 1) # batch_size*(filter_numlen(filters))
out = self.dropout(out)
out = self.fc(out) # batch_sizelabel_num
return outif name == ‘main’:
print(‘running the TextCNN…’)