人工智能原理基础——仅用中学数学知识就能看懂(下篇)

源码文件
链接: https://pan.baidu.com/s/1j4ny5voep7tUpz7u2gSl_Q?pwd=2hmg 提取码: 2hmg
系列合集
人工智能原理基础——仅用中学数学知识就能看懂(上篇)

  • 上篇:代价函数、梯度下降、前向/反向传播、激活函数、keras、深度学习入门

人工智能原理基础——仅用中学数学知识就能看懂(中篇)

  • 中篇:卷积神经网络及实战

人工智能原理基础——仅用中学数学知识就能看懂(下篇)

  • 下篇:循环神经网络及实战

十三、LSTM网络:自然语言处理实践

原理

​ 上一篇博客中我们已经完整的讲述了利用词嵌入,把句子转化为词向量序列的详细过程。最后我们说因为无法忽视语言数据在时间上的关联性,所以我们的神经网络必须要有处理这种关联的能力,现在我们就来看看如何做到这一点,为了讲解上的清晰,我们我们不再考虑词嵌入这个数据的预处理操作,就假设句子里的每个词都已经被处理成了一个合适的300位词向量,那上一节我们采用全连接神经网络做预测的时候,是把所有的词向量平铺开然后送入,现在我们就来改造一下神经网络的工作模式。

首先为了让改造过程的讲解简明一些,我们把网络结构图的画法做个简化,忽略具体的神经元之间的连线,用箭头表示数据的输入和输出,然后我们把这个句子中每个词向量分别命名为x1、x2、x3和x4,第一步先把句子的第一个词向量x1作为隐藏层的输入,直到输出a1,这时候还没完,还不到最后输出预测值的时候,我们把这个输出值a1保存起来。第二步再拿出句子的第二个词,向量x2和第一个词向量输出的结果,a1一起作为这一步的输入,得到本次的输出,a2继续保存 a2。第三步再拿出句子的第三个词,向量x3,同样和上一次的输出a2一起作为本次的输入得到输出,a3一直到最后一步句子的最后一个词向量x4和 a3一起输入得到输出a4,此时我们才让a4经过输出层得到最后的预测输出,这样一个句子,每个词对最后预测输出的影响就在每一次的保存并和下一步数据的共同作用中持续到了最后,这样序列中的数据就产生了循环的特点,所以我们把这样改造的神经网络称之为循环神经网络RNN

image-20250301165257969

首先循环神经网络中的激活函数多采用双曲正切函数tanh,而不是relu。像keras这种编程框架中的循环神经网络,默认使用的激活函数就是tanh。循环神经网络中的第一步将x1送入网络中不像后面那样有上一个输出反送回来的数据一起作为输入,但为了保证每一步操作的统一性,我们一般也会手动添加一个共同的输入,比如一个全0的向量,下图展示RNN在时间上的每一步流程示意,得到a4之后再经过激活函数softmax或者relu输出最后的预测值,而反向传播则是从最后的输出层开始,把误差从相反的方向依次从后往前传播,当然反向传播的过程也是按照时间进行的,换句话说我们让时光逐步倒流进行反向传播。

image-20250301173341394

循环神经网络虽然也可以堆叠成多层的,但是人们轻易不会构造的太深。一般来说2~3层就可以了,因为循环神经网络会在时间上展开,所以网络结构将会变得很大,训练起来相对于其他的结构更加的困难,虽然这种标准的循环神经网络可以在一定程度上应对这种在时间上有依赖的序列问题,但是对于常依赖的问题效果就不太好了。

image-20250301170259993

我们举个例子,假如有这么一句话,上海电视台记者在北京街头采访了一名来自四川的年轻人,他性格开朗热爱生活,家乡有一种著名的动物叫做如果要预测这句话中最后缺失的那个词是什么单看后面部分作为具有代表性的著名动物,老虎、扬子鳄、袋鼠等等都是合理的。但是根据前面可知地点在四川,那这个词大概率是熊猫而不是其他的动物,但难搞的是四川这个词距离后面非常的远,换句话说依赖的路径十分的长,标准的RNN结构在这种长依赖问题上表现并不好,所以为了解决这个问题,人们在循环神经网络上做了进一步的改造,其中最为著名的就是==LSTM长短时记忆网络(Long Short-term Memory)==。

image-20250301170403408

我们就来看看LSTM结构相比于标准的RNN结构构到底发生了哪些改变?

首先LSTM结构中的输出再次经过一个tanh函数,而原先的输出则变成了一个叫做==细胞状态==的东西,这个细胞状态就是LSTM结构能够应对长依赖问题的关键,因为它能够让网络具有记忆和遗忘的效果。这样既然输出变成了细胞状态和输出两个部分,那么作为一个循环中的某一步,这个结构的输入必然相应的也就变成了上一步的细胞状态和上一步的输出。我们继续,本次输入数据也不仅仅依靠上一次的输出和本次的词向量,而把上一个细胞状态也一起作为输入,我们用加法符号把这两个部分加在一起,这就是LSTM主要的数据流转和运算的过程,而为了实现记忆和遗忘,LSTM结构使用了两个门来实现。

  • 第一个叫==遗忘门==的实现很简单,使用一个sigmoid,网络层我们知道sigmoid层的输出值都是在0~1之间,如果我们让 sigmoid层的输出和上个细胞状态值相乘,那么sigmoid的输出层为0时,结果就相当于把上个细胞值全部丢弃,换句话说全部忘记;为1时,则上次的细胞值全部保留,换句话说全部记忆,而如果处于0~1之间,则相当于遗忘掉部分或者说记忆部分,而 sigmoid层的输入则是本次的词向量和上一次的输出合并的数据,所以这样就可以通过本次和之前数据共同决定忘记多少之前的细胞值,比如在前面那句话中遇到四川这个词的时候,就知道地点发生了变化,从而忘记北京和上海这两个地点,我们让本次的词向量和上一次输出合并的数据
  • 再经过一个sigmoid的层形成另外一个控制门,控制在之前标准RNN结构中用来更新的部分,换句话说这个门用来控制是否更新本次的细胞状态值,这就是第二个门==更新门==,这样网络就会选择重要的词汇更新细胞状态,比如遇到四川这个词的时候,就选择更新我们的细胞状态

而除了记忆和遗忘这两个门以外,最终的输出还有一个==输出门==,我们说细胞状态值经过一个tanh函数之后,就是本次的输出值,LSTM结构让他也被一个门控制着(通过Sigmoid层),这样就可以在遇到重要词汇的时候产生强输出,而不重要的时候产生弱输出,这就是LSTM模型的工作原理。

image-20250301172632003

从对其原理的基本讲解可以看出来,它之所以能够应对更长的序列依赖,正是因为除了输入和输出以外,它还添加了细胞状态的概念。如果我们单独看这个细胞状态的传递路径,你就会发现只要遗忘门和记忆门训练的得当,就能让像“四川”这种关键词在循环的过程中被传递得很长,甚至到最后一步,从而达到长时记忆的效果,而像“性格”这种对预测不重要的词,也能在短时间内被遗忘,从而达到短时记忆的效果。这就是LSTM长短时记忆这个名字的由来。

image-20250301172717537

LSTM网络是上个世纪90年代就已经出现了一种循环神经网络结构,在循环神经网络中有着极高的地位。后来人们又提出了各种变种的循环神经网络,很多都是基于LSTM的改进,其中最为著名的就是GRU网络,它简化了LSTM结构,很多时候效果和LSTM也很接近,所以目前大家都乐于使用GRU结构。GRU结构很简单,这里给大家推荐一个很有名的Understanding LSTM Networks这篇博客详细地解释了LSTM,最后介绍了一些包括GRE在内的LSTM的种结构,大家可以在课后自己研究一下,相比于LSTM,GRU做了哪些改变和简化。

image-20250301173037465

编程实验

本次编程实验我们接着上一节的文本情感分类,采用循环神经网络看看效果。

  • 关于第三方预训练词向量的下载方法
    • 访问github上Chinese-Word-Vectors项目的地址:https://github.com/Embedding/Chinese-Word-Vectors
    • 点击红框处下载(代码演示的是红框中的版本)image-20250301174543159
    • 这个1.7G的预训练词向量文件的加载过程十分的长(看电脑配置),需要耐心等待。

这一次我们的代码编写起来就很简单,只需要把上节课的全连接层神经网络改为循环神经网络就好,

循环神经网络不适宜深层堆叠,对于我们这里的问题效果也不大,所以我们就用两层,当然我们之前说过多层的循环神经网络RNN也好,LSTM也好,前面的层需要在序列的每一步都输出结果作为下一步的输入,所以我们需要给第一层的LSTM配置上return_sequence=True,这个tas是参数就表示每一步都输出结果的意思,默认值是false表示只在最后一步输出结果,所以这里我们把它配置为True。

这样我们就把网络改造成为了一个使用LSTM结构的循环神经网络,最后我们把嵌入层的trainable设置为False,冻结我们的嵌入层参数,看看在不训练词向量的情况下模型的效果。

import shopping_data
from keras.preprocessing import sequence
from keras.layers import Dense, Embedding, Flatten
from keras.models import Sequential
from keras.layers import LSTM
import chinese_vec
import numpy as np

import os
os.environ["CUDA_VISIBLE_DEVICES"] = "-1"  # Disable GPU

x_train, y_train, x_test, y_test = shopping_data.load_data()

print('x_train.shape', x_train.shape)
print('y_train.shape', y_train.shape)
print('x_test.shape', x_test.shape)
print('y_test.shape', y_test.shape)

print(x_train[0])
print(y_train[0])

# 传入训练集和测试集的文本
# vocalen返回这个词典的词汇数量
# word_index返回在全部语料中的索引词典
vocalen, word_index = shopping_data.createWordIndex(x_train, x_test)
print(vocalen)
print(word_index)

# 传入训练集x_train和词典word_index就得到了训练数据的索引表示
x_train_index = shopping_data.word2Index(x_train, word_index)
x_test_index = shopping_data.word2Index(x_test, word_index)

# 数据对齐
maxlen = 25
x_train_index = sequence.pad_sequences(x_train_index, maxlen)
x_test_index = sequence.pad_sequences(x_test_index, maxlen)



# 构造神经网络
model = Sequential()
model.add(Embedding(trainable=False, input_dim=vocalen, output_dim=300, input_length=maxlen))
model.add(LSTM(128, return_sequences=True)) # 128表示输出数据的维度,我们先让他输出128维
model.add(LSTM(128)) # 两层LSTM

# 因为是二分类问题,所以输出层单个神经元
model.add(Dense(1, activation='sigmoid'))
# 代价函数是一个适用于二分类问题的交叉熵代价函数,优化器我们使用的是adam而不是sgd,因为序列问题一般都比较难以训练,所以我们使用这个更快的优化器
# adam是一种使用动量的自适应优化器,比普通的sgd要快,效果更好
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])

# 开始训练
model.fit(x_train_index, y_train, epochs=200, batch_size=512)

# 测试
score, acc = model.evaluate(x_test_index, y_test)

print('Test Score:',score)
print('Test Accuracy', acc)

image-20250301190724332

我们将trainable=True

image-20250301190800271

最后在测试集上的准确率是85.6%,这好像跟我们上节课差不多,这是怎么回事呢?那是这样的,当我们把嵌入层的训练打开之后,嵌入层的参数矩阵就会同步参与网络的训练,而我们这里嵌入层的参数数量有300×9512,就要比后面的神经网络中的参数还要多,所以在训练的时候嵌入层的参数就会占用很大的比重。换句话说训练的重心跑到了词嵌入矩阵参数上,所以导致了LSTM和普通的全连接层再打开嵌入层的训练(trainable=true)之后变的差距不是很大,而我们这里的语料数据集又不是很丰富,所以最后的词向量训练的效果必然不会很好,所以我们说更常用的方法是使用别人在海量数据集上训练出来的词向量。

那么我们来看看如何操作:

import shopping_data
from keras.preprocessing import sequence
from keras.layers import Dense, Embedding, Flatten
from keras.models import Sequential
from keras.layers import LSTM
import chinese_vec
import numpy as np

import os
os.environ["CUDA_VISIBLE_DEVICES"] = "-1"  # Disable GPU

x_train, y_train, x_test, y_test = shopping_data.load_data()

print('x_train.shape', x_train.shape)
print('y_train.shape', y_train.shape)
print('x_test.shape', x_test.shape)
print('y_test.shape', y_test.shape)

print(x_train[0])
print(y_train[0])

# 传入训练集和测试集的文本
# vocalen返回这个词典的词汇数量
# word_index返回在全部语料中的索引词典
vocalen, word_index = shopping_data.createWordIndex(x_train, x_test)
print(vocalen)
print(word_index)

# 传入训练集x_train和词典word_index就得到了训练数据的索引表示
x_train_index = shopping_data.word2Index(x_train, word_index)
x_test_index = shopping_data.word2Index(x_test, word_index)

# 数据对齐
maxlen = 25
x_train_index = sequence.pad_sequences(x_train_index, maxlen)
x_test_index = sequence.pad_sequences(x_test_index, maxlen)

# 自行构造词嵌入矩阵
# load_word_vec函数返回值是一个python的字典类型,键是词汇,值是对应的词向量
word_vecs = chinese_vec.load_word_vecs()
# 将预训练的词向量提取出来之后,想办法把我们的嵌入矩阵给替换掉
# 先自己创建一个一会儿用来替换的嵌入矩阵
embedding_matrix = np.zeros(vocalen, 300) # 这个矩阵的形状就是我们情感分类任务中全部语料词汇的数量,或者说词汇表的数量乘以每个词向量的维度300
                                                # 实际上就是我们一会用来替换的嵌入层之前自己构建的嵌入矩阵的形状

# 然后我们从刚才加载出来的预训练词向量集合中找到这些词的词向量,把它们填充到Embedding_matrix之中
# word是词典中的一个文本,i作为键,从预训练的词向量集合word_vecs中找到这个词的词向量
for word, i in word_index.items():
    embedding_vector = word_vecs.get(word)
    # 不过我们需要先判断一下是不是None,因为有可能我们的语料中的词并不在预训练词向量集合之中
    if embedding_vector is not None:
        # 如果找到了,我们就把我们所创建的词嵌入矩阵的第i行替换成已经训练好的词向量
        embedding_matrix[i] = embedding_vector
        # 这样一趟下来我们自己创建的矩阵embedding_matrix就是一个用别人训练好的词向量构成的词嵌入矩阵,这样我们就可以把嵌入层的嵌入矩阵给替换掉

# 构造神经网络
model = Sequential()
# 我们配置一下Embedding层的weights参数,
model.add(Embedding(trainable=False, weights=[embedding_matrix], input_dim=vocalen, output_dim=300, input_length=maxlen))
model.add(LSTM(128, return_sequences=True)) # 128表示输出数据的维度,我们先让他输出128维
model.add(LSTM(128)) # 两层LSTM

# 因为是二分类问题,所以输出层单个神经元
model.add(Dense(1, activation='sigmoid'))
# 代价函数是一个适用于二分类问题的交叉熵代价函数,优化器我们使用的是adam而不是sgd,因为序列问题一般都比较难以训练,所以我们使用这个更快的优化器
# adam是一种使用动量的自适应优化器,比普通的sgd要快,效果更好
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])

# 开始训练
model.fit(x_train_index, y_train, epochs=200, batch_size=512)

# 测试
score, acc = model.evaluate(x_test_index, y_test)

print('Test Score:',score)
print('Test Accuracy', acc)

结果接近百分之九十,效果大为提升

image-20250301192321308

我们把上节课和这节课几种方案,最后在测试集上的准确率我们都排列出来,趋势很明显,不使用预训练词向量,也不自己训练,这样的效果最差,自己训练能够好一点,使用第三方预训练的词向量的效果最好,同时LSTM的效果在几种情况下都要比普通的全连接神经网络效果要好,这就看出来了,循环神经网络确实在序列问题上技高一筹。

百分之九十,效果大为提升

image-20250301192321308

我们把上节课和这节课几种方案,最后在测试集上的准确率我们都排列出来,趋势很明显,不使用预训练词向量,也不自己训练,这样的效果最差,自己训练能够好一点,使用第三方预训练的词向量的效果最好,同时LSTM的效果在几种情况下都要比普通的全连接神经网络效果要好,这就看出来了,循环神经网络确实在序列问题上技高一筹。

image-20250301192425804

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值