任务链接:https://wx.zsxq.com/dweb/#/index/222248424811
1.卷积
参考链接:https://zhuanlan.zhihu.com/p/57575810
1.1卷积
卷积:图像中不同数据窗口的数据和卷积核(一个滤波矩阵)作内积的操作叫做卷积。其计算过程又称为滤波,本质是提取图像不同频段的特征。
卷积核:也称为滤波器filter,带着一组固定权重的神经元,通常是n*m二维的矩阵,n和m也是神经元的感受野。
卷积层:多个滤波器叠加便成了卷积层。
卷积流程示例图:
1.2转置卷积
转置卷积:向普通卷积方向相反的转换,即执行上采样。例子:生成高分辨率图像、将低维特征图映射到高维空间。
在卷积中,我们定义C为卷积核,Large为输入图像,Small为输出图像。经过卷积(矩阵乘法)后,我们将大图像下采样为小图像。这种矩阵乘法的卷积的实现遵照:C x Large = Small
下面的例子展示了这种运算的工作方式。它将输入平展为 16×1 的矩阵,并将卷积核转换为一个稀疏矩阵(4×16)。然后,在稀疏矩阵和平展的输入之间使用矩阵乘法。之后,再将所得到的矩阵(4×1)转换为 2×2 的输出。
现在,如果我们在等式的两边都乘上矩阵的转置 CT,并借助「一个矩阵与其转置矩阵的乘法得到一个单位矩阵」这一性质,那么我们就能得到公式 CT x Small = Large,如下图所示。
这里可以看到,我们执行了从小图像到大图像的上采样。这正是我们想要实现的目标。
1.3扩张卷积
扩张卷积就是通过在核元素之间插入空格来使核「膨胀」。新增的参数 l(扩张率)表示我们希望将核加宽的程度。具体实现可能各不相同,但通常是在核元素之间插入 l-1 个空格。下面展示了 l = 1, 2, 4 时的核大小。
在这张图像中,3×3 的红点表示经过卷积后,输出图像是 3×3 像素。尽管所有这三个扩张卷积的输出都是同一尺寸,但模型观察到的感受野有很大的不同。l=1 时感受野为 3×3,l=2 时为 7×7。l=3 时,感受野的大小就增加到了 15×15。有趣的是,与这些操作相关的参数的数量是相等的。我们「观察」更大的感受野不会有额外的成本。因此,扩张卷积可用于廉价地增大输出单元的感受野,而不会增大其核大小,这在多个扩张卷积彼此堆叠时尤其有效。
1.4可分卷积
1.4.1空间可分卷积
空间可分卷积操作的是图像的 2D 空间维度,即高和宽。从概念上看,空间可分卷积是将一个卷积分解为两个单独的运算。对于下面的示例,3×3 的 Sobel 核被分成了一个 3×1 核和一个 1×3 核。
在卷积中,3×3 核直接与图像卷积。在空间可分卷积中,3×1 核首先与图像卷积,然后再应用 1×3 核。这样,执行同样的操作时仅需 6 个参数,而不是 9 个。
此外,使用空间可分卷积时所需的矩阵乘法也更少。给一个具体的例子,5×5 图像与 3×3 核的卷积(步幅=1,填充=0)要求在 3 个位置水平地扫描核(还有 3 个垂直的位置)。总共就是 9 个位置,表示为下图中的点。在每个位置,会应用 9 次逐元素乘法。总共就是 9×9=81 次乘法。
另一方面,对于空间可分卷积,我们首先在 5×5 的图像上应用一个 3×1 的过滤器。我们可以在水平 5 个位置和垂直 3 个位置扫描这样的核。总共就是 5×3=15 个位置,表示为下图中的点。在每个位置,会应用 3 次逐元素乘法。总共就是 15×3=45 次乘法。现在我们得到了一个 3×5 的矩阵。这个矩阵再与一个 1×3 核卷积,即在水平 3 个位置和垂直 3 个位置扫描这个矩阵。对于这 9 个位置中的每一个,应用 3 次逐元素乘法。这一步需要 9×3=27 次乘法。因此,总体而言,空间可分卷积需要 45+27=72 次乘法,少于普通卷积。
尽管空间可分卷积能节省成本,但深度学习却很少使用它。一大主要原因是并非所有的核都能分成两个更小的核。如果我们用空间可分卷积替代所有的传统卷积,那么我们就限制了自己在训练过程中搜索所有可能的核。这样得到的训练结果可能是次优的。
1.4.2深度可分卷积
在描述这些步骤之前,有必要回顾一下我们之前介绍的 2D 卷积核 1×1 卷积。首先快速回顾标准的 2D 卷积。举一个具体例子,假设输入层的大小是 7×7×3(高×宽×通道),而过滤器的大小是 3×3×3。经过与一个过滤器的 2D 卷积之后,输出层的大小是 5×5×1(仅有一个通道)。
一般来说,两个神经网络层之间会应用多个过滤器。假设我们这里有 128 个过滤器。在应用了这 128 个 2D 卷积之后,我们有 128 个 5×5×1 的输出映射图(map)。然后我们将这些映射图堆叠成大小为 5×5×128 的单层。通过这种操作,我们可将输入层(7×7×3)转换成输出层(5×5×128)。空间维度(即高度和宽度)会变小,而深度会增大。
现在使用深度可分卷积,看看我们如何实现同样的变换。
首先,我们将深度卷积应用于输入层。但我们不使用 2D 卷积中大小为 3×3×3 的单个过滤器,而是分开使用 3 个核。每个过滤器的大小为 3×3×1。每个核与输入层的一个通道卷积(仅一个通道,而非所有通道!)。每个这样的卷积都能提供大小为 5×5×1 的映射图。然后我们将这些映射图堆叠在一起,创建一个 5×5×3 的图像。经过这个操作之后,我们得到大小为 5×5×3 的输出。现在我们可以降低空间维度了,但深度还是和之前一样。
深度可分卷积——第一步:我们不使用 2D 卷积中大小为 3×3×3 的单个过滤器,而是分开使用 3 个核。每个过滤器的大小为 3×3×1。每个核与输入层的一个通道卷积(仅一个通道,而非所有通道!)。每个这样的卷积都能提供大小为 5×5×1 的映射图。然后我们将这些映射图堆叠在一起,创建一个 5×5×3 的图像。经过这个操作之后,我们得到大小为 5×5×3 的输出。
在深度可分卷积的第二步,为了扩展深度,我们应用一个核大小为 1×1×3 的 1×1 卷积。将 5×5×3 的输入图像与每个 1×1×3 的核卷积,可得到大小为 5×5×1 的映射图。
因此,在应用了 128 个 1×1 卷积之后,我们得到大小为 5×5×128 的层。
通过这两个步骤,深度可分卷积也会将输入层(7×7×3)变换到输出层(5×5×128)。
下图展示了深度可分卷积的整个过程。
|
所以,深度可分卷积有何优势呢?效率!相比于 2D 卷积,深度可分卷积所需的操作要少得多。
1.5分组卷积
在分组卷积中,过滤器会被分为不同的组。每一组都负责特定深度的典型 2D 卷积。下面的例子能让你更清楚地理解。
上图展示了具有两个过滤器分组的分组卷积。在每个过滤器分组中,每个过滤器的深度仅有名义上的 2D 卷积的一半。它们的深度是 Din/2。每个过滤器分组包含 Dout/2 个过滤器。第一个过滤器分组(红色)与输入层的前一半([:, :, 0:Din/2])卷积,而第二个过滤器分组(橙色)与输入层的后一半([:, :, Din/2:Din])卷积。因此,每个过滤器分组都会创建 Dout/2 个通道。整体而言,两个分组会创建 2×Dout/2 = Dout 个通道。然后我们将这些通道堆叠在一起,得到有 Dout 个通道的输出层。
小卷积核VS大卷积核
在AlexNet中有有11x11的卷积核与5x5的卷积核,但是在VGG网络中因为层数增加,卷积核都变成3x3与1x1的大小啦,这样的好处是可以减少训练时候的计算量,有利于降低总的参数数目。关于如何把大卷积核替换为小卷积核,本质上有两种方法。
1.将二维卷积差分为两个连续一维卷积
二维卷积都可以拆分为两个一维的卷积,这个是有数学依据的,所以11x11的卷积可以转换为1x11与11x1两个连续的卷积核计算,总的运算次数:
- 11x11 = 121次
- 1x11+ 11x1 = 22次
2.将大二维卷积用多个连续小二维卷积替代
可见把大的二维卷积核在计算环节改成两个连续的小卷积核可以极大降低计算次数、减少计算复杂度。同样大的二维卷积核还可以通过几个小的二维卷积核替代得到。比如:5x5的卷积,我们可以通过两个连续的3x3的卷积替代,比较计算次数
- 5x5= 25次
- 3x3+ 3x3=18次
2.池化层
池化层的具体操作与卷基层的操作基本相同,只不过池化层的卷积核为只取对应位置的最大值、平均值等(最大池化、平均池化),并且不经过反向传播的修改。池化的时候同样需要提供filter的大小、步长、下面就是3x3步长为1的filter在5x5的输入图像上均值池化计算过程与输出结果
改用最大值做池化的过程与结果如下:
3.TextCNN的原理
参考链接:https://www.cnblogs.com/bymo/p/9675654.html
TextCNN的详细过程原理图如下:
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 个最大的信息,保留了全局的序列信息。
4.TextCNN代码
import os
import numpy as np
import tensorflow as tf
from collections import Counter
import tensorflow.contrib.keras as kr
class Text(object):
# 打开文件
def open_file(self, filename, mode='r'):
return open(filename, mode, encoding='utf-8', errors='ignore')
# 读取文件
def read_file(self, filename):
contents, labels = [], []
with self.open_file(filename) as f:
for line in f:
try:
label, content = line.strip().split('\t')
if content:
contents.append(list(content))
labels.append(label)
except:
pass
return contents, labels
# 读取词汇表,一个词对应一个id
def read_vocab(self, vocab_dir):
with self.open_file(vocab_dir) as fp:
words = [_.strip() for _ in fp.readlines()]
word_to_id = dict(zip(words, range(len(words))))
return words, word_to_id
# 读取分类目录,一个类别对应一个id
def read_category(self):
categories = ['体育', '财经', '房产', '家居', '教育', '科技', '时尚', '时政', '游戏', '娱乐']
cat_to_id = dict(zip(categories, range(len(categories))))
return categories, cat_to_id
# 根据训练集构建词汇表,存储
def build_vocab(self, train_dir, vocab_dir, vocab_size=5000):
data_train, _ = self.read_file(train_dir)
all_data = []
for content in data_train:
all_data.extend(content)
counter = Counter(all_data)
count_pairs = counter.most_common(vocab_size - 1)
words, _ = list(zip(*count_pairs))
# 添加一个 <PAD> 来将所有文本pad为同一长度
words = ['<PAD>'] + list(words)
self.open_file(vocab_dir, mode='w').write('\n'.join(words) + '\n')
# 将文件转换为id表示
def process_file(self, filename, word_to_id, cat_to_id, max_length=600):
contents, labels = self.read_file(filename)
data_id, label_id = [], []
for i in range(len(contents)):
data_id.append([word_to_id[x] for x in contents[i] if x in word_to_id])
label_id.append(cat_to_id[labels[i]])
# 使用keras提供的pad_sequences来将文本转为固定长度,不足的补0
x_pad = kr.preprocessing.sequence.pad_sequences(data_id, max_length)
y_pad = kr.utils.to_categorical(label_id, num_classes=len(cat_to_id)) # 将标签转换为one-hot表示
return x_pad, y_pad
# 获取数据
def get_data(self, filenname, text_length):
vocab_dir = './data/cnews/cnews.vocab.txt'
categories, cat_to_id = text.read_category()
words, word_to_id = text.read_vocab(vocab_dir)
x, y = text.process_file(filenname, word_to_id, cat_to_id, text_length)
return x, y
class TextCNN(object):
def __init__(self):
self.text_length = 600 # 文本长度
self.num_classer = 10 # 类别数
self.vocab_size = 5000 # 词汇表达小
self. word_vec_dim = 64 # 词向量维度
self.filter_width = 5 # 卷积核尺寸
self.filter_width_list = [2, 3, 4, 5] # 卷积核尺寸列表
self.num_filters = 10 # 卷积核数目
self.dropout_prob = 0.5 # dropout概率
self.learning_rate = 0.005 # 学习率
self.iter_num = 10 # 迭代次数
self.batch_size = 64 # 每轮迭代训练多少数据
self.model_save_path = './model/' # 模型保存路径
self.model_name = 'mnist_model' # 模型的命名
self.embedding = tf.get_variable('embedding', [self.vocab_size, self.word_vec_dim])
self.fc1_size = 32 # 第一层全连接的神经元个数
self.fc2_size = 64 # 第二层全连接的神经元个数
self.fc3_size = 10 # 第三层全连接的神经元个数
# 模型1,使用多种卷积核
def model_1(self, x, is_train):
# embedding层
embedding_res = tf.nn.embedding_lookup(self.embedding, x)
pool_list = []
for filter_width in self.filter_width_list:
# 卷积层
conv_w = self.get_weight([filter_width, self.word_vec_dim, self.num_filters], 0.01)
conv_b = self.get_bias([self.num_filters])
conv = tf.nn.conv1d(embedding_res, conv_w, stride=1, padding='VALID')
conv_res = tf.nn.relu(tf.nn.bias_add(conv, conv_b))
# 最大池化层
pool_list.append(tf.reduce_max(conv_res, reduction_indices=[1]))
pool_res = tf.concat(pool_list, 1)
# 第一个全连接层
fc1_w = self.get_weight([self.num_filters * len(self.filter_width_list), self.fc1_size], 0.01)
fc1_b = self.get_bias([self.fc1_size])
fc1_res = tf.nn.relu(tf.matmul(pool_res, fc1_w) + fc1_b)
if is_train:
fc1_res = tf.nn.dropout(fc1_res, 0.5)
# 第二个全连接层
fc2_w = self.get_weight([self.fc1_size, self.fc2_size], 0.01)
fc2_b = self.get_bias([self.fc2_size])
fc2_res = tf.nn.relu(tf.matmul(fc1_res, fc2_w) + fc2_b)
if is_train:
fc2_res = tf.nn.dropout(fc2_res, 0.5)
# 第三个全连接层
fc3_w = self.get_weight([self.fc2_size, self.fc3_size], 0.01)
fc3_b = self.get_bias([self.fc3_size])
fc3_res = tf.matmul(fc2_res, fc3_w) + fc3_b
return fc3_res
# 模型2,使用一个卷积核
def model_2(self, x, is_train):
# embedding层
embedding_res = tf.nn.embedding_lookup(self.embedding, x)
# 卷积层
conv_w = self.get_weight([self.filter_width, self.word_vec_dim, self.num_filters], 0.01)
conv_b = self.get_bias([self.num_filters])
conv = tf.nn.conv1d(embedding_res, conv_w, stride=1, padding='VALID')
conv_res = tf.nn.relu(tf.nn.bias_add(conv, conv_b))
# 最大池化层
pool_res = tf.reduce_max(conv_res, reduction_indices=[1])
# 第一个全连接层
fc1_w = self.get_weight([self.num_filters, self.fc1_size], 0.01)
fc1_b = self.get_bias([self.fc1_size])
fc1_res = tf.nn.relu(tf.matmul(pool_res, fc1_w) + fc1_b)
if is_train:
fc1_res = tf.nn.dropout(fc1_res, 0.5)
# 第二个全连接层
fc2_w = self.get_weight([self.fc1_size, self.fc2_size], 0.01)
fc2_b = self.get_bias([self.fc2_size])
fc2_res = tf.nn.relu(tf.matmul(fc1_res, fc2_w) + fc2_b)
if is_train:
fc2_res = tf.nn.dropout(fc2_res, 0.5)
# 第三个全连接层
fc3_w = self.get_weight([self.fc2_size, self.fc3_size], 0.01)
fc3_b = self.get_bias([self.fc3_size])
fc3_res = tf.matmul(fc2_res, fc3_w) + fc3_b
return fc3_res
# 定义初始化网络权重函数
def get_weight(self, shape, regularizer):
w = tf.Variable(tf.truncated_normal(shape, stddev=0.1))
tf.add_to_collection('losses', tf.contrib.layers.l2_regularizer(regularizer)(w)) # 为权重加入L2正则化
return w
# 定义初始化偏置项函数
def get_bias(self, shape):
b = tf.Variable(tf.ones(shape))
return b
# 生成批次数据
def batch_iter(self, x, y):
data_len = len(x)
num_batch = int((data_len - 1) / self.batch_size) + 1
indices = np.random.permutation(np.arange(data_len)) # 随机打乱一个数组
x_shuffle = x[indices] # 随机打乱数据
y_shuffle = y[indices] # 随机打乱数据
for i in range(num_batch):
start = i * self.batch_size
end = min((i + 1) * self.batch_size, data_len)
yield x_shuffle[start:end], y_shuffle[start:end]
# 训练
def train(cnn, X_train, y_train):
x = tf.placeholder(tf.int32, [None, cnn.text_length])
y = tf.placeholder(tf.float32, [None, cnn.num_classer])
y_pred = cnn.model_1(x, True)
# 声明一个全局计数器,并输出化为0,存放到目前为止模型优化迭代的次数
global_step = tf.Variable(0, trainable=False)
# 损失函数,交叉熵
cross_entropy = tf.nn.softmax_cross_entropy_with_logits(logits=y_pred, labels=y)
loss = tf.reduce_mean(cross_entropy)
# 优化器
train_step = tf.train.AdamOptimizer(learning_rate=cnn.learning_rate).minimize(loss, global_step=global_step)
saver = tf.train.Saver() # 实例化一个保存和恢复变量的saver
# 创建一个会话,并通过python中的上下文管理器来管理这个会话
with tf.Session() as sess:
# 初始化计算图中的变量
init_op = tf.global_variables_initializer()
sess.run(init_op)
# 通过checkpoint文件定位到最新保存的模型
ckpt = tf.train.get_checkpoint_state(cnn.model_save_path)
if ckpt and ckpt.model_checkpoint_path:
# 加载最新的模型
saver.restore(sess, ckpt.model_checkpoint_path)
# 循环迭代,每次迭代读取一个batch_size大小的数据
for i in range(cnn.iter_num):
batch_train = cnn.batch_iter(X_train, y_train)
for x_batch, y_batch in batch_train:
loss_value, step = sess.run([loss, train_step], feed_dict={x: x_batch, y: y_batch})
print('After %d training step(s), loss on training batch is %g.' % (i, loss_value))
saver.save(sess, os.path.join(cnn.model_save_path, cnn.model_name), global_step=global_step)
# 预测
def predict(cnn, X_test, y_test):
# 创建一个默认图,在该图中执行以下操作
x = tf.placeholder(tf.int32, [None, cnn.text_length])
y = tf.placeholder(tf.float32, [None, cnn.num_classer])
y_pred = cnn.model_1(x, False)
saver = tf.train.Saver() # 实例化一个保存和恢复变量的saver
correct_prediction = tf.equal(tf.argmax(y, 1), tf.argmax(y_pred, 1)) # 判断预测值和实际值是否相同
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32)) # 求平均得到准确率
with tf.Session() as sess:
ckpt = tf.train.get_checkpoint_state(cnn.model_save_path)
if ckpt and ckpt.model_checkpoint_path:
saver.restore(sess, ckpt.model_checkpoint_path)
# 根据读入的模型名字切分出该模型是属于迭代了多少次保存的
global_step = ckpt.model_checkpoint_path.split('/')[-1].split(' ')[-1]
# 计算出测试集上准确
accuracy_score = sess.run(accuracy, feed_dict={x: X_test, y: y_test})
print('After %s training step(s), test accuracy = %g' % (global_step, accuracy_score))
else:
print('No checkpoint file found')
return
if __name__ == '__main__':
text_length = 600 # 文本长度
text = Text()
X_train, y_train = text.get_data('./data/cnews/cnews.train.txt', text_length) # X_train shape (50000, 300)
X_test, y_test = text.get_data('./data/cnews/cnews.test.txt', text_length) # X_test shape (10000, 300)
X_val, y_val = text.get_data('./data/cnews/cnews.val.txt', text_length) # X_val shape (5000, 300)
is_train = True
cnn = TextCNN()
if is_train:
train(cnn, X_train, y_train)
else:
predict(cnn, X_val, y_val)