现在每次后面都会加上一个Q&A部分,就是每次有人看完,讲完后的问题,或者是一些不全面的方面,以问答的形式呈现出来。
现在开的坑系列是Github上一个即将3k+Star的NLP-tutorial项目,里面是一些NLP方面的Deep-learning代码,框架Tensor和Torch都有,而且实现行数基本都控制在了100行以内,比较适合去研究一下。这样之后搭框架的时候就会明白许多了。
项目地址:https://github.com/graykode/nlp-tutorial
第二部分是CNN卷积网络模型
这部分是基于非常经典的paper, Convolutional Neural Networks for Sentence Classification,https://arxiv.org/pdf/1408.5882.pdf
这份代码就是展现了这个里面的模型代码。所以首先先介绍一下这个论文的内容。
这篇也相当于是填了我之前的一个坑,在去年10月初学的时候就接触到了这篇论文,但是直到现在才有机会去细看,真是挖了个大坑啊。
https://blog.csdn.net/Raymond_MY/article/details/83240140
CNN在图像处理领域取得了很大的成就,它的卷积和池化结构能很好提取图像的信息,而在 NLP 领域RNN则使用的更多,RNN 及其各种变种,比如LSTM等,拥有记忆功能,使得它们更擅长处理上下文。但 NLP 领域很多方面使用 CNN 取得了出色的效果,比如语义分析、查询检索、文本分类等任务。这篇文章使用用 CNN 进行情感分类
本文以预训练好的词向量矩阵表示一个句子,并且将其作为卷积神经网络的输入层,再通过标记好的数据训练出神经网络模型从而达到预测数据类别的效果。
将NLP与CNN结合在一起,获得了成功,引起了人们对于CNN在NLP领域应用的研究兴趣。
图中最左边的部分即为输入层,总的来说输入层就是句子对应的矩阵。一般不会使用 ont-hot 向量来表示单词,而是使用 k 维的分布式词向量。那么对于一个长度为 n 的句子,则构成一个 n × k 的矩阵。
所以,可以设 xi 为句子的第 i 个单词,它为 k 维向量。那么一个句子为
其中表示串联的意思。
另外,根据对词向量的作用可以分为两种模式:静态和非静态。
图中第二部分为卷积层,卷积层的作用就是用于提取句子的特征。主要是通过一个 h × k 的卷积核 w 在输入层从上到下进行滑动进行卷积操作,通过该卷积操作得到一个 feature map。feature map 的列为1,行为 (n-h+1)。即
其中
上图中输入层上红色框就是卷积操作的卷积核,可以看到它是 2 × k 维的,运算后变为 feature map 的一个元素。除此之外,还可以将 h 定为3,此时卷积核变为 3 × k 维,如图中黄色框框部分。相同维度的可以有若干个参数不同的卷积核,所以最终在每种维度下都可以得到若干个 feature map。
卷积操作的意义是什么?可以看到它其实是根据 h 大小不同提取不同长度相邻单词的特征。
图中第三部分为池化层,池化层的作用是对特征做进一步提取,将最重要的特征提取出来。这里使用的是 max-over-time pooling 操作,即取出 feature map 中的最大值作为最重要的特征,即cˆ=max{c}c^=max{c}。所以最终对于每个 feature map 池化后都得到一个一维向量,取最大值作为特征也解决了不同句子长短的问题,尽管短的句子会用 0 进行填充,但通过取最大值消除了该问题。
前面的通过卷积层的多个不同卷积核操作得到若干 feature map,而再经过池化层处理后得到若干个一维向量。
图中最后部分为全连接层,全连接层通过使用 softmax 分类器得到各个分类的概率。前面的池化层的输出以全连接的形式连到 softmax 层,softmax 层定义好分类。
import numpy as np
import torch
import torch.nn as nn # torch的神经网络库,里面有很多基本的神经网络基础代码,比如Conv2d,ReLU等
import torch.optim as optim # optimizer的简称,是实现各种优化算法的包,比如梯度下降,比如Adam
# 这个库中提供了类和函数用来对任意标量函数进行求导,引入Variable可以实现自动求导
from torch.autograd import Variable
# 一个函数库,里面有卷积函数con2d,poolong函数,还有很多非线性激活函数
# 和nn中的函数不同,不仅从调用方式上有区别,nn.functional.xxx则要同时输入数据和weight,bias等参数
# nn.functional.xxx需要自己定义weight,每次调用时都需要手动传入weight,而nn.xxx则不用
import torch.nn.functional as F
# 在pyTorch中,基本的数据结构是Torch,包含了多维张量,可以就把它看作是n维矩阵的一个表示
dtype = torch.FloatTensor # 创建一个浮点tensor,还没有对其赋值和规定其维度大小
# Text-CNN Parameter
embedding_size = 2 # n-gram 每次看两个词
sequence_length = 3 # 句子的长度
num_classes = 2 # 0 or 1 分类标签
filter_sizes = [2, 2, 2] # n-gram window # 滑动窗口
num_filters = 3 # filter的数量
# 3 words sentences (=sequence_length is 3)
sentences = ["i love you", "he loves me", "she likes baseball", "i hate you", "sorry for that", "this is awful"]
labels = [1, 1, 1, 0, 0, 0] # 1 is good, 0 is not good.
# 以下两行代码是将上面sentences列表中的单词提取出来
word_list = " ".join(sentences).split() # 每句首先使用空格分割形成一个单词列表
word_list = list(set(word_list)) # 用一个小技巧,先让list变成set,然后再变回去,这样就提取出了单词列表
# 以下两行是建立单词对应序号的索引字典word_dict和序号对应单词的索引number_dict
# 使用了enumerate函数,使得在遍历的同时可以追踪到序号,i, w是元组,其实可以写成(i, w)
word_dict = {w: i for i, w in enumerate(word_list)} # w: i 单词对应序号键值对
vocab_size = len(word_dict) # number of Vocabulary
#
inputs = []
for sen in sentences:
inputs.append(np.asarray([word_dict[n] for n in sen.split()]))
#将标签放入targets列表中,复制一遍是为了防止原本数据被更改
targets = []
for out in labels:
targets.append(out) # To using Torch Softmax Loss function
# 以下两行将输入进行Variable包装,保持在计算过程中数据类型的一致
input_batch = Variable(torch.LongTensor(inputs))
target_batch = Variable(torch.LongTensor(targets))
# CNN模型部分
class TextCNN(nn.Module): # 定义网络时一般是继承torch.nn.Module创建新的子类
def __init__(self): # 构造函数
super(TextCNN, self).__init__() # 子类构造函数强制调用父类构造函数
self.num_filters_total = num_filters * len(filter_sizes) # 计算filter的总数
# W是词向量
self.W = nn.Parameter(torch.empty(vocab_size, embedding_size).uniform_(-1, 1)).type(dtype)
# 权重
self.Weight = nn.Parameter(torch.empty(self.num_filters_total, num_classes).uniform_(-1, 1)).type(dtype)
# 偏置值
self.Bias = nn.Parameter(0.1 * torch.ones([num_classes])).type(dtype)
# 前向传播过程
def forward(self, X):
embedded_chars = self.W[X] # [batch_size, sequence_length, sequence_length]
embedded_chars = embedded_chars.unsqueeze(1) # add channel(=1) [batch, channel(=1), sequence_length, embedding_size]
pooled_outputs = []
for filter_size in filter_sizes:
# conv : [input_channel(=1), output_channel(=3), (filter_height, filter_width), bias_option]
conv = nn.Conv2d(1, num_filters, (filter_size, embedding_size), bias=True)(embedded_chars)
h = F.relu(conv)
# mp : ((filter_height, filter_width))
mp = nn.MaxPool2d((sequence_length - filter_size + 1, 1))
# pooled : [batch_size(=6), output_height(=1), output_width(=1), output_channel(=3)]
pooled = mp(h).permute(0, 3, 2, 1)
pooled_outputs.append(pooled)
h_pool = torch.cat(pooled_outputs, len(filter_sizes)) # [batch_size(=6), output_height(=1), output_width(=1), output_channel(=3) * 3]
h_pool_flat = torch.reshape(h_pool, [-1, self.num_filters_total]) # [batch_size(=6), output_height * output_width * (output_channel * 3)]
model = torch.mm(h_pool_flat, self.Weight) + self.Bias # [batch_size, num_classes]
return model
model = TextCNN() # 初始化模型
# 损失函数定义为交叉熵损失函数
criterion = nn.CrossEntropyLoss()
# 采用Adam优化算法,学习率0.001
optimizer = optim.Adam(model.parameters(), lr=0.001)
Training 训练过程,5000轮
for epoch in range(5000):
optimizer.zero_grad() # 初始化
output = model(input_batch)
# output : [batch_size, num_classes], target_batch : [batch_size] (LongTensor, not one-hot)
loss = criterion(output, target_batch)
if (epoch + 1) % 1000 == 0:
print('Epoch:', '%04d' % (epoch + 1), 'cost =', '{:.6f}'.format(loss))
# 自动求导反向传播,使用step()来更新参数
loss.backward()
optimizer.step()
# Test 测试部分
test_text = 'sorry hate you' # 测试集就只有这一句话
tests = [np.asarray([word_dict[n] for n in test_text.split()])] # 把每个词放到tests中
test_batch = Variable(torch.LongTensor(tests)) # 构造测试batch
# Predict 预测结果
predict = model(test_batch).data.max(1, keepdim=True)[1]
if predict[0][0] == 0:
print(test_text,"is Bad Mean...")
else:
print(test_text,"is Good Mean!!")