【程序喵笔记】RNN实现文本分类

编程入门 行业动态 更新时间:2024-10-25 12:24:13

【程序喵笔记】RNN实现<a href=https://www.elefans.com/category/jswz/34/1771357.html style=文本分类"/>

【程序喵笔记】RNN实现文本分类

RNN实现文本分类

目标:运用RNN网络对影评数据集进行分类

词向量模型 word2vec

词向量概述

一句话很长,由千千万万个词构成,计算机想要明白一句话,就要先认识一个个词,计算机擅长处理什么?是数字啊,所以我们就把“词”转化成一个个数字组成“向量”。我们希望得到一个 大表embedding maxtrix,每个词对应的向量都能找到。

这个词向量不能随便设定的吧,首先需要相近的词汇离得近一点,举个栗子,一个二维向量(a,b)代表一个词。比如“man”这个词在空间中(1,5),boy(1,4),而water(-1,-4),我们看看在坐标系下这些点。

很显然,man和boy离得很近,离water很远。但是二维远远不能满足对一个词的描述,通常情况下,我们用上百维的向量来描述一个词,两个词之间的关联程度用余弦距离或者欧式距离表示。还是这三个词,用更高维度标签,不同颜色代表不同数字范围,对应数字越大表示越深红。

圈出的部分明显可以看出,相似的词是有很大的相似程度的。但是词向量这么多维的数字人为的编造也太麻烦了,我们可以用一个神经网络来构造每个词对应的向量。

词向量模型

既然是神经网络,那肯定就要有输入和输出,词不可能单个的蹦出来,一定是有上下文关系的,我们就利用这一点,就可以构建词向量模型的网络啦,假设我们的词向量是一个4维向量。

举个甜甜的表白词:You are the one I have been looking for. (你就是我一直在追寻的幸福),依次先圈出5个词(滑动窗口),找到中间的词w(t)和这个词的上下文 w(t-2), w(t-1), w(t+1), w(t+2)

可以将上下文作为输入,中间词作为输出,来判断判断输出这个词是不是上下文所对应的中间词。先建立一个词向量大表embedding,里面有所有词和对应的向量(随机初始化).

前向传播:根据输入词,找到大表中对应的向量,经过神经网络得到各种词的概率,判断输出是否为中间词。反向传播:不仅更新网络模型,也会更新词向量的大表.

常见有两种模型,CBOW是上下文为输入,中间词的输出。而skip-gram正好相反,但是原理相近。

这样虽然可以找到词向量,但是最后的预测概率是一个超多的分类问题,需要的参数可是太多了,那我们能不能简化一点,比较输入和输出是否相邻,那就变成一个二分类问题,是相邻的为1,不是为0,以skip-gram为例。

原先的输入和输出左边,右边是改进后增加target

这样看上去没啥大问题,但是,所有标签全为1啊,并没有0,这样神经网络还学啥,任意两个词都是相邻的了。所以,在这里,我们增加一个负采样,就是人为的创造一些与不相邻的数据,让target为0.

负采样的个数,根据模型而定,滑动窗口的大小和每个窗口取几个值也是根据模型而定,大体的思路就是这样的。总结一下,三大步:

1.初始化词向量矩阵

词汇个数:vocabulary_size,每个向量维度vector_size,相当于一个大表,每个词对应向量,这也是最终所需要的

2.构建数据

将文本做滑动窗口处理,找到中间词和上下文对应词向量,比如the用一个4维向量表示,you、are、his等一些词也在词向量表格找到对应4维向量。

3.构建网络更新参数

通过神经网络返向传播来计算更新,此时不仅更新网络的权重参数矩阵,也会更新词向量矩阵(绿色的4维向量)。

模型实现

1.数据预处理

构建语料库,根据语料库数目限制和最小词频限制,构建出 47135个词作为语料库。

max_vocabulary_size = 50000 # 语料库最大词语数
min_occurrence = 10 # 最小词频# 读取数据
data_path = 'text8.zip'
with zipfile.ZipFile(data_path) as f:text_words = f.read(f.namelist()[0]).lower().split()#总共17005207词,我们只选取最常见的# 创建计数器,从多到少计算词频
# 第一个词为‘UNK’,文本中不在语料库中的词汇用这个代替
count = [('UNK', -1)]
# 基于词频返回max_vocabulary_size个常用词
count.extend(collections.Counter(text_words).most_common(max_vocabulary_size - 1))# 剔除掉出现次数少于'min_occurrence'的词
for i in range(len(count) - 1, -1, -1):if count[i][1] < min_occurrence:count.pop(i)else:break

词-ID映射,语料库中每个词给出对应ID,并将文本转化为ID

# 每个词都分配一个ID
word2id = dict()
for i, (word, _)in enumerate(count):word2id[word] = i# 所有词转换成ID
data = list()
unk_count = 0
for word in text_words:index = word2id.get(word, 0)if index == 0:unk_count += 1data.append(index)
count[0] = ('UNK', unk_count)
# id2word是word2id反映射
id2word = dict(zip(word2id.values(), word2id.keys()))

文本text_words 根据 word2id 变成了一个个数字索引 data

构建词向量矩阵,将索引转化维词向量

embedding_size = 200 # 词向量由200维向量构成
embedding = tf.Variable(tf.random.normal([vocabulary_size, embedding_size])) #维度:47135, 200#通过tf.nn.embedding_lookup函数将索引转换成词向量
def get_embedding(x):x_embed = tf.nn.embedding_lookup(embedding, x)return x_embed

2.滑动窗口提取数据对

滑动窗口大小为2*skip_window + 1,即为7,中间的作为输入,左右两边随机选择 num_skips个词作为标签,构成一对数据。

skip_window = 3 # 左右窗口大小
num_skips = 2 # 一次制作多少个输入输出对
batch_size = 128 #一个batch下有128组数据

在一个 batch下取数据,即一次取128组数据对,每个窗口取2组,在128/2个窗口下取即可

def next_batch(batch_size, num_skips, skip_window):global data_indexassert batch_size % num_skips == 0assert num_skips <= 2 * skip_windowbatch = np.ndarray(shape=(batch_size), dtype=np.int32)labels = np.ndarray(shape=(batch_size, 1), dtype=np.int32)# 数据窗口为7span = 2 * skip_window + 1 # 创建队列,长度为7buffer = collections.deque(maxlen=span)#创建一个长度为7的队列if data_index + span > len(data):#如果文本被滑完,从头再来data_index = 0# 比如一个队列为deque([5234, 3081, 12, 6, 195, 2, 3134], maxlen=7),数字代表词IDbuffer.extend(data[data_index:data_index + span])data_index += spanfor i in range(batch_size // num_skips):context_words = [w for w in range(span) if w != skip_window]#上下文为[0, 1, 2, 4, 5, 6]words_to_use = random.sample(context_words, num_skips)#在上下文里随机选2个候选词for j, context_word in enumerate(words_to_use):batch[i * num_skips + j] = buffer[skip_window]#输入都为当前窗口的中间词,即3labels[i * num_skips + j, 0] = buffer[context_word]#标签为当前候选词# 窗口右移,如果文本读完,从头再来if data_index == len(data):buffer.extend(data[0:span])data_index = spanelse:buffer.append(data[data_index])data_index += 1data_index = (data_index + len(data) - span) % len(data)return batch, labels

3.迭代优化

计算损失,损失为 nce损失,同时加入负采样。先分别计算出正样本和采样出的负样本对应的输出(0到1之间数字),再通过 sigmoid交叉熵来计算损失。

num_sampled = 64 # 负采样个数
# nce权重和偏差
nce_weights = tf.Variable(tf.random.normal([vocabulary_size, embedding_size]))
nce_biases = tf.Variable(tf.zeros([vocabulary_size]))
# 定义nce损失,x_emded为转化为词向量的中间词,y为上下文词
def nce_loss(x_embed, y):with tf.device('/cpu:0'):y = tf.cast(y, tf.int64)loss = tf.reduce_mean(tf.nn.nce_loss(weights=nce_weights,biases=nce_biases,labels=y,inputs=x_embed,num_sampled=num_sampled,num_classes=vocabulary_size))return loss

计算梯度,更新 embeddingnce参数

with tf.GradientTape() as g:x, y = next_batch(batch_size, num_skips, skip_window)emb = get_embedding(x)loss = nce_loss(emb, y)# 计算梯度
gradients = g.gradient(loss, [embedding, nce_weights, nce_biases])
# 更新
optimizer = tf.optimizers.SGD(learning_rate)
optimizer.apply_gradients(zip(gradients, [embedding, nce_weights, nce_biases]))

4.验证

找一些词,看看这个词邻近的词的8个词(计算向量余弦相似度),’‘nine’'很明显,邻近单词都是数字。

5.补充

大多数情况下,词向量模型不需要我们自己训练,有网上训练好的,直接down下来,但是一些特殊任务有大量特殊词汇,需要自己训练。

文本分类任务

整体思路

  • 数据集:一条影评对应一个标签,标签为0/1,消极评价/积极评价
  • 词向量模型:加载训练好的词向量模型。/ 50维词向量
  • 序列网络模型:双向RNN模型进行识别

将文本以固定长度 max_len截取,每个词转化为id,再转化为词向量,输入RNN网络进行二分类。

数据准备

word2id语料表

根据词频构建语料表,即训练集出现的词

# 构建语料表,基于词频来进行统计
counter = Counter()
with open('./data/train.txt',encoding='utf-8') as f:for line in f:line = line.rstrip()label, words = line.split('\t')words = words.split(' ')counter.update(words)# <pad>用作文本补齐,长度不够填充
words = ['<pad>'] + [w for w, freq in counter.most_common() if freq >= 10]
print('Vocab Size:', len(words))# 保存文件
Path('./vocab').mkdir(exist_ok=True)
with open('./vocab/word.txt', 'w',encoding='utf-8') as f:for w in words:f.write(w+'\n')

读取文件并标号得到word2id映射字典

word2idx = {}
with open('./vocab/word.txt',encoding='utf-8') as f:for i, line in enumerate(f):line = line.rstrip()word2idx[line] = i

构建训练数据

运用数据生成器data_generator生成数据和标签,并运用tf.data.Dataset.from_generator(data_generator,output_data_type,output_data_shape)不断读取

# 定义生成器
def data_generator(f_path, params):with open(f_path,encoding='utf-8') as f:print('Reading', f_path)for line in f:line = line.rstrip()# 标签和文本分别保存label, text = line.split('\t')text = text.split(' ')# 每个词对应id,利用word2id字典查找x = [params['word2idx'].get(w, len(word2idx)) for w in text]# 文本长度保持一致if len(x) >= params['max_len']:#截断操作,max_len = 1000 x = x[:params['max_len']]else:x += [0] * (params['max_len'] - len(x))#补齐操作y = int(label)yield x, y

把数据放入batch中,训练数据加入 shuffle操作

词向量模型

embedding矩阵

下载词向量表是一个词对应50维向量,但是我们的语料表可能有一部分单词出现在下载向量的表中,没有出现记为 unknown ,找到到训练集语料表每个词索引所对应的向量,即 embedding

#一个大表,里面有20598个词和一个‘unknown’,每个词有50维向量【20599*50】
embedding = np.zeros((len(word2idx)+1, 50)) with open('./data/glove.6B.50d.txt',encoding='utf-8') as f: #下载的词向量映射count = 0for i, line in enumerate(f):# 提取词和词向量line = line.rstrip()sp = line.split(' ')word, vec = sp[0], sp[1:]# 判断词是否出现在我们的语料表中if word in word2idx:count += 1embedding[word2idx[word]] = np.asarray(vec, dtype='float32') #将词转换成对应的向量
np.save('./vocab/word.npy', embedding)

embedding矩阵为一个 20599*50维度的数组,每行为语料表单词所对应的向量。

embedding_lookup

有了embedding矩阵和文本序列的词id,利用 tf.nn.embedding_lookup 就可以将输入的序列转化为序列向量

词向量模型是整体网络结构中的一层,但是词向量模型不会更新参数。

序列网络模型

双向RNN

RNN从前到后走一遍,得到隐层特征h,双向RNN在实现的时候,再从后到前走一遍,又得到另一组h,两组h拼接一起就是双向RNN的隐层生成

实现很简单,只需要封装一个 Bidirectional,比如 self.rnn1 = tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(params['rnn_units'], return_sequences=True))。这样隐层输出会有 2*rnn_units个隐层特征。

网络构架

增加了三层双向RNN网络,每舱网络有200*2维隐层特征,并添加 dropout,最后全连接层。

改进:

RNN训练的时候速度很慢,文本长度1000,要1000长度的时间序列以词输入RNN。为了提高速度,利用 reshape,保证一次输入10长度的时间序列输入RNN,再利用 reduce_max将RNN隐层10长度的时间序列压缩维1个长度的序列,再进入下一层RNN,先 reshapereduce_max,这样可以提高运算速度。

x = tf.reshape(x, (batch_sz*10*10, 10, 50)) #改变尺寸,只有10长度的时间序列输入RNN 
x = self.drop1(x, training=training)
x = self.rnn1(x)
x = tf.reduce_max(x, 1)

训练

损失:softmax交叉熵

学习率:学习率随着步长指数下降。decay_lr = tf.optimizers.schedules.ExponentialDecay(params['lr'], 1000, 0.95)

评估:验证集准确率三次不下降就停止训练。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C07eZrCc-1587818125274)(E:\Tensorflow\0.我的笔记\6.RNN实现文本分类\image\20.训练结果.png)]

uce_max`,这样可以提高运算速度。

x = tf.reshape(x, (batch_sz*10*10, 10, 50)) #改变尺寸,只有10长度的时间序列输入RNN 
x = self.drop1(x, training=training)
x = self.rnn1(x)
x = tf.reduce_max(x, 1)

训练

损失:softmax交叉熵

学习率:学习率随着步长指数下降。decay_lr = tf.optimizers.schedules.ExponentialDecay(params['lr'], 1000, 0.95)

评估:验证集准确率三次不下降就停止训练。

更多推荐

【程序喵笔记】RNN实现文本分类

本文发布于:2024-03-04 06:43:05,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1708532.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:文本   笔记   程序   RNN

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!