王树森《RNN & Transformer》系列公开课

本课程主要介绍NLP相关,包括RNN、LSTM、Attention、Transformer、BERT等模型,以及情感识别、文本生成、机器翻译等应用

ShusenWang的个人空间-ShusenWang个人主页-哔哩哔哩视频 (bilibili.com)


(一)NLP基础

1、数据处理基础

数值特征 (Numeric Features,特点是可以比较大小)和类别特征(Categorical Features)

类别特征需转换成数值特征,但不能只用一个标量表示 (因为类别特征做数值运算无意义),需要用one-hot编码

  • Age为numeric feature,可以直接保留,不做处理
  • Gender为binary feature(二元特征),如0表示为女性,1表示为男性
  • Nationality为categorical feature,需编码成数值向量。先转换成1~类别数 之间的整数(建立一个字典来映射),再one-hot encoding (因为国籍映射成的整数不能表示大小)【为什么要从1开始?0保留来表示缺失或未知的数据,one-hot后为全0向量

大约有197个国籍

文本处理:

每个单词就是一个类别,单词就是categorical feature,把单词变为数值向量

  1. **Tokenization(Text to Word):**把文本(字符串)分割为单词列表
  2. **计算词频:**计算每个单词出现的次数(哈希表,Key为word,Value为词频)。若word已经存在在哈希表里,将其value+1;若不存在,添加(word,1)进入哈希表
  3. 排序哈希表:让word按照词频递减的顺序排列
  4. 把词频换为index,词频最高的word的 index=1
  5. 统计词频的目的是:保留常用词,去掉低频词(减小字典里单词的个数,即vocabulary)
  6. **one-hot encoding:**通过查字典,把每个word映射成一个正整数,再把这个正整数变成one-hot向量(one-hot向量维度=vocabulary)
  7. 在字典里找不到的word,编码时可以忽略这个词,也可以编码成全0向量

为什么要去掉低频词?

一种情况是Name entities(姓名实体,无意义),或拼写错误

另一种原因是不希望vocabulary太大,vocabulary越大,one-hot向量维度越高,会让计算变慢,同时模型参数也会越多,容易过拟合

2、文本处理与词嵌入(Word Embedding)

The IMDB Movie Review Dataset,判断电影评论的情感是正面还是负面(二分类问题)

5w条电影评论,2w5k作为训练数据,2w5k作为测试数据

Text to Sequence

(1)tokenization(一个token就是一个单词,或一个字符):

  • 通常会将大写转为小写(但Apple若转为apple,语义会发生变化)
  • 去掉停用词(stop words),如:the、a、of等最高频的单词(对二分类没有帮助)
  • 拼写纠错

(2)dictionary: 统计词频、去掉低频词;让每个单词对应一个正整数。有了字典,就可以把每个单词映射为一个整数。这样一来,一句话就可以用一个正整数的列表表示,称为sequences序列

(3)one-hot encoding:

**(4)align sequences(序列对齐):**sequence长度不同,训练数据没有对齐,因为要把数据存储在矩阵或者张量里,序列需要对齐,每条序列有相同的长度

  • 假设序列长度为w,砍掉前面的词,只保留最后w个词(或保留前面w个词也可以)
  • 如果不到w个词,做zero padding,用null来补齐,至长度为w(从前面补齐,或从后面)

**总结:**一条评论用一个正整数的序列(sequence)来表示,sequence就是神经网络中Embedding层的输入。还需要对齐不同sequence的长度

Word Embedding(Word to Vector)

(1)One-Hot Encoding: 字典里一共有v个单词,需要维度为v的one-hot向量(很容易维度过高;RNN的参数数量正比于输入向量的维度

**(2)Word Embedding:**把高维one-hot向量映射为低维向量(d为词向量的维度,由用户自己决定;v是vocabulary,即字典里单词的数量)

参数矩阵P的每一行都是一个词向量,矩阵的行数是v,每一行对应一个单词;d由用户决定,d的大小会影响机器学习模型的表现,应由cross validation来选择一个比较好的d

参数矩阵是从训练数据中学习出来的

(3)keras提供Embedding层

python 复制代码
from keras.models import Sequential
from keras.layers import Flatten, Dense, Embedding

embedding_dim = 8

# vocabulary大小 v
# 词向量维度 d (通过cross validation选出)
# 每个Sequence的长度
model.add(Embedding(vocabulary, embedding_dim, input_length=word_num))

Embedding层的输出 是一个 (word_num, embedding_dim) 的矩阵,Embedding层的参数数量 = vocabulary × embedding_dim

  • 每条评论中有 word_num 个词,每个单词用 embedding_dim 的词向量来表示
  • Embedding层中有一个参数矩阵P,矩阵P的 行数 = vocabulary,列数 = embedding_dim

**总结:**Embedding层把每个单词映射成一个 embedding_dim 的词向量

Logistic Regression做二分类,判断电影评论是正面还是负面

(1)用keras实现一个分类器:

  • Sequential:把神经网络的层按顺序搭起来,返回model对象,往里依次添加各种层
  • **Embedding:**输出为 (word_num, embedding_dim) 的矩阵【参数数量 = vocabulary × embedding_dim】
  • **Flatten:**把 word_num × embedding_dim 的矩阵压扁,变为向量
  • **Dense(全连接层,即Logistic Regression):**输出是1维的,用sigmoid激活函数,输出为0-1之间的数(0代表负面评价,1代表正面评价)【参数数量 = word_num × embedding_dim + 1,+1是指偏移量bias】
  • summary() 函数可以打印出模型的概要:每一层的名字Layer(type)、输出的大小Output Shape、参数的数量Param #
python 复制代码
from keras.models import Sequential
from keras.layers import Flatten, Dense, Embedding

embedding_dim = 8

model = Sequential()

# vocabulary大小 v
# 词向量维度 d (通过cross validation选出)
# 每个Sequence的长度
model.add(Embedding(vocabulary, embedding_dim, input_length=word_num))
model.add(Flatten())
model.add(Dense(1, activation='sigmoid'))

model.summary()  # 打印出模型概要

(2)接下来编译模型:

  • 分为 训练数据train(2w条电影评论) 和 验证数据valid(5k条电影评论)
  • 把train全部扫一遍为一个epoch,每一个epoch都会输出训练loss、acc和验证loss、acc
python 复制代码
from keras import optimizers

epochs = 50
model.compile(optimizer=optimizers.RMSprop(lr=0.0001), loss='binary_crossentropy', metrics=['acc'])
# 用训练数据来拟合模型
history = model.fit(x_train, y_train, epochs=epochs, batch_size=32, validation_data=(x_valid, y_valid))

x_train是个2w*20的矩阵(20指每条电影评论中有20个单词,每个单词用正整数表示)
Performance on the training and validation sets

(3)在测试集上检验模型表现:

python 复制代码
loss_and_acc = model.evaluate(x_test, labels_test)
print('loss=' + str(loss_and_acc[0]))
print('acc=' + str(loss_and_acc[1]))

电影评论texts,首先做tokenization,变为tokens;

然后把每个tokens编码为一个数字,这样一来,一条电影评论就可以用一个正整数的序列sequence 来表示(sequence即神经网络中Embedding层的输入);

由于电影评论的长短不一,得到的sequence长短也不一,故还需要对齐(长度>w,只保留后w个词;长度<w,从前面用null补齐至w)。

输入Embedding层【参数数量 = vocabulary × embedding_dim】,把每个单词映射到一个 embedding_dim 维的词向量;

再用Flatten,将矩阵压扁成向量;

最后用**Logistic分类器【参数数量 = word_num × embedding_dim + 1】**输出一个0-1之间的数


(二)RNNs(Recurrent Neural Networks)

  • **one-to-one模型(一个输入对应一个输出):**如全连接神经网络和CNN。适合处理图片(输入一张图片,输出每一类的概率值)
  • many-to-one模型或many-to-many模型(输入和输出长度都不固定):RNN。适合文本、语音等Sequential data(时序序列数据)

1、Simple RNN模型

训练数据足够多时,RNN效果不如Transformer;但在小规模问题,RNN很有用

(1)Simple RNN

  • **状态向量h:**积累阅读过的信息(ht中包含了x0~xt的输入信息)
  • 词向量x:按顺序读取每一个词向量
  • 参数矩阵A:一开始随机初始化,从训练数据中学习

每次把一个词向量输入RNN,RNN就会更新状态h,把新的输入积累到状态h里(h0包含了第一个词的信息,h1包含了前两个词的信息,以此类推)

更新状态h的时候需要用到参数矩阵A(**不论链路多长,都只有一个参数矩阵A。**A随机初始化,利用训练数据来学习A)

SimpleRNN怎么把输入的词向量x结合到状态h里?

激活函数是 tanh(双曲正切函数),输入是任意实数,输出在 -1 ~ 1。为什么要用tanh?【每次更新状态h之后,做一个normalization,让h恢复到 -1~1 之间

  • 假设输入的词向量 x0 = ... = x100 = 0,h100 = Ah99 = A² h98 = ... = A^100 h0
  • 若矩阵A最大的特征值<1,新的状态每个元素都趋于0
  • 若矩阵A最大的特征值>1,新的状态每个元素都巨大,状态向量会爆炸(数值为nan=not a number)

新的状态ht,是旧状态ht-1,和新的输入xt 的函数

**可训练参数:**参数矩阵A(可能还有intercept vector,即偏置项)

(2)Simple RNN for IMDB Review

  • **Word Embedding:**词映射为向量x,词向量的维度d由cross-validation确定最优维度【输出维度:(word_num, state_dim)】
  • **SimpleRNN Layer:**输入是词向量,输出是状态h(维度也由cross-validation确定最优维度)【输出维度:state_dim的向量,若只输出RNN最后一个状态向量ht;(word_num, state_dim),若输出所有状态向量】
  • 可以输出所有h,也可以只输出最后一个状态向量ht(积累了整句话的信息)
  • ht输出分类器,输出0-1之间的数值(0代表负面评价,1代表正面评价)
python 复制代码
# (1)搭建模型
from keras.models import Sequential
from keras.layers import SimpleRNN, Embedding, Dense

vocabulary = 10000   # unique words in the dictionary
embedding_dim = 32    # shape(x)  词向量x的维度
word_num = 500      # sequence length   每个评论长度为500个单词
state_dim = 32     # shape(h)   状态向量h的维度

model = Sequential()
model.add(Embedding(vocabulary, embedding_dim, input_length=word_num))   # 词映射为向量
# return_sequences=False 指RNN只输出最后一个状态向量,之前的状态向量全扔掉
model.add(SimpleRNN(state_dim, return_sequences=False))   # 指定状态向量h的维度 state_dim
model.add(Dense(1, activation='sigmoid'))   # 全连接层,输入RNN的最后一个状态h,输出一个0-1之间的数

model.summary()

# (2)编译模型
from keras import optimizers

epochs = 3   # Early stopping防止过拟合(在validation accuracy变差之前就停止)
model.compile(optimizer=optimizers.RMSprop(lr=0.001), loss='binary_crossentropy', metrics=['acc'])
# 用训练数据来拟合模型
history = model.fit(x_train, y_train, epochs=epochs, batch_size=32, validation_data=(x_valid, y_valid))

# (3)用测试数据评价模型表现
loss_and_acc = model.evaluate(x_test, labels_test)
print('loss=' + str(loss_and_acc[0]))
print('acc=' + str(loss_and_acc[1]))

**RNN层参数数量:**shape(h) × (shape(h) + shape(x)) + bias/intercept ,第一项为矩阵A的大小,第二项为RNN默认使用intercept(偏移量)

上述做法是只保留了最后一个状态ht,丢弃了前面所有状态。也可以保留h0~ht,此时RNN输出为一个矩阵(每行就是一个状态h),需要加Flatten层把状态矩阵变成向量。向量作为分类器的输入,来判断电影是正面的还是负面的

python 复制代码
from keras.models import Sequential
from keras.layers import SimpleRNN, Embedding, Dense

vocabulary = 10000   # unique words in the dictionary
embedding_dim = 32    # shape(x)  词向量x的维度
word_num = 500      # sequence length   每个评论长度为500个单词
state_dim = 32     # shape(h)   状态向量h的维度

model = Sequential()
model.add(Embedding(vocabulary, embedding_dim, input_length=word_num))   # 词映射为向量
model.add(SimpleRNN(state_dim, return_sequences=True))   # 指定状态向量h的维度 state_dim
model.add(Flatten())
model.add(Dense(1, activation='sigmoid'))   # 全连接层

model.summary()

(3)缺点

**不擅长long-term dependence:**状态h100跟100步之前的输入x1几乎没关系(即后面的状态会遗忘之前的输入)

RNN适合文本、语音等时序序列数据

RNN按顺序读取每一个词向量,并在状态向量h中积累看到过的信息,如h1中包含x0和x1的信息,以此类推,ht中包含了之前所有输入的信息,可以认为,ht就是RNN从整个输入序列中抽取的特征向量

RNN记忆很短,会遗忘很久之前的输入x。若时间序列很长,比如好几十步,最终的ht已经忘了早先的输入

SimpleRNN有一个参数矩阵A,维度是 shape(h) × (shape(h)+shape(x)),一开始随机初始化,从训练数据中学习。可能还有一个intercept向量

注意,不管时序多长,参数矩阵只有一个,所有模块里的参数都是一样的

2、LSTM(Long Short Term Memory)

LSTM的记忆会比SimpleRNN长很多,但也还是有遗忘的问题。LSTM是一种RNN模型,可以避免梯度消失的问题,可以有更长的记忆(一般用RNN,都是LSTM,SimpleRNN基本不用)

  • 每当读取一个新的输入x,就会更新状态h
  • SimpleRNN只有一个参数矩阵,LSTM有四个参数矩阵
    • 遗忘门有一个参数矩阵Wf(sigmoid映射到0~1)
    • 输入门有两个参数矩阵:Wi(sigmoid映射到0~1)、Wc(tanh映射到 -1~1)
    • 输出门有一个参数矩阵Wo(sigmoid映射到0~1)

(1)内部结构

**传输带Ct:**过去的信息直接送到下一个时刻,以此避免梯度消失

LSTM中有很多个gate(遗忘门、输入门、输出门),可以有选择地让信息通过

  • forget gate(遗忘门): 由 sigmoid函数 和 element-wise multiplication/哈达玛积(两个向量的每个元素对应相乘,结果也是个向量)组成。有选择地让传输带C的值通过(假如f 对应的元素为0,c对应的元素就不能通过,对应的输出为0;假如f 对应的元素为1,c对应的元素就全部通过,对应的输出为c本身)


遗忘门f有选择的让传输带c的元素通过

遗忘门有一个参数矩阵Wf,需要通过反向传播从训练数据里学习

  • **input gate(输入门):**参数矩阵Wi(sigmoid映射到0~1)、参数矩阵Wc(tanh映射到 -1~1)


new value

更新传输带C:

  • 用 遗忘门ft 和 传输带旧的值Ct-1 算element-wise multiplication**(遗忘门ft 可以选择性地遗忘 Ct-1中的一些元素)**
  • 计算 输入门it 和 新的值Ct 的element-wise multiplication**(加入新的信息)**
  • **output gate(输出门):**计算ot,参数矩阵Wo(sigmoid函数映射到0~1)

**计算状态向量ht:**一份传到下一步,另一份是LSTM的输出

到第t步为止,一共有t个向量x输入了LSTM,可以认为所有这些x向量的信息都积累在了状态ht里

(2)参数数量

遗忘门、输入门、new value、输出门,共有4个参数矩阵,共有 4 × shape(h) × [shape(h) + shape(x)]

  • 矩阵的行数:shape(h)
  • 列数:shape(h) + shape(x)

(3)keras实现LSTM

让LSTM只输出最后一个状态向量ht,即从电影评论中提取出的特征向量,再输入线性分类器,来判断评论是正面的还是负面的

python 复制代码
from keras.models import Sequential
from keras.layers import LSTM, Embedding, Dense, Flatten

vocabulary = 10000   # unique words in the dictionary
embedding_dim = 32    # shape(x)  词向量x的维度
word_num = 500      # sequence length   每个评论长度为500个单词
state_dim = 32     # shape(h)   状态向量h的维度

model = Sequential()
model.add(Embedding(vocabulary, embedding_dim, input_length=word_num))   # 词映射为向量
# return_sequences=False 指RNN只输出最后一个状态向量,之前的状态向量全扔掉
model.add(LSTM(state_dim, return_sequences=False))   # 指定状态向量h的维度 state_dim
model.add(Dense(1, activation='sigmoid'))   # 全连接层

model.summary()

只输出最后一个状态向量h

  • 每个参数矩阵:shape(h) × [shape(h) + shape(x)] + shape(h)(LSTM默认使用intercept)
  • LSTM参数量:*4

可以加dropout(设置为某个0-1之间的数字即可):

python 复制代码
model.add(LSTM(state_dim, return_sequences=False), dropout=0.2)

若加dropout没有提升测试准确率,原因:虽然训练时出现了overfitting,但overfitting不是由LSTM造成的,而是由Embedding层造成的,故对LSTM使用dropout正则化没有用

LSTM和SimpleRNN的区别是用了一条传输带,让过去的信息可以很容易的传输到下一时刻,这样就有了更长的记忆

LSTM有4个组件,分别是:forget gate(遗忘门)、input gate(输入门)、new value(新的输入)、output gate(输出门),这4个组件各自有一个参数矩阵,所以一共有4个参数矩阵,参数数量为 4 × shape(h) × [shape(h) + shape(x)]

3、Making RNNs More Effective

三个技巧来提升RNN的效果(对所有RNN都适用)

(1)Stacked RNN(多层RNN)

  • 把很多全连接层堆叠起来:multi-layer perceptron
  • 把很多卷积层堆叠起来:深度卷积网络
  • 把很多RNN层堆叠起来:多层RNN网络

神经网络每一步都会更新状态h,有两份:一份送到下一时刻,一份作为输出(同时也是下一层的输入)

python 复制代码
# 多层LSTM 用keras实现
from keras.models import Sequential
from keras.layers import LSTM, Embedding, Dense

vocabulary = 10000   # unique words in the dictionary
embedding_dim = 32    # shape(x)  词向量x的维度
word_num = 500      # sequence length   每个评论长度为500个单词
state_dim = 32     # shape(h)   状态向量h的维度

model = Sequential()
model.add(Embedding(vocabulary, embedding_dim, input_length=word_num))   # 词映射为向量
# return_sequences=True 第一层的输出会成为第二层的输入,故要输出所有的状态向量h
model.add(LSTM(state_dim, return_sequences=True, dropout=0.2))   # 指定状态向量h的维度 state_dim
model.add(LSTM(state_dim, return_sequences=True, dropout=0.2)) 
model.add(LSTM(state_dim, return_sequences=False, dropout=0.2))   # 只输出最后一个状态向量
model.add(Dense(1, activation='sigmoid'))   # 全连接层,输入第三层LSTM最后一个状态向量,输出分类结果
  • Embedding层输出:(word_num, embedding_dim)
  • 第一层LSTM输出:(word_num, state_dim) return_sequences=True(输出500个状态向量h)
  • 第二层LSTM输出:(word_num, state_dim) return_sequences=True(输出500个状态向量h)
  • 第三层LSTM输出:state_dim维的向量 return_sequences=False(最后一个状态,相当于从word_num个词里提取的特征向量)

实验结果跟单层RNN效果差不多,猜想是由于Embedding层参数太多,没有足够的数据把这一层训练好,出现overfitting,加再多LSTM层也无济于事

(2)Bidirectional RNN(双向RNN)

训练两条RNN,一条从左往右,一条从右往左,两条RNN完全独立,不共享参数和状态 。两条RNN各自输出自己的状态向量,然后把它们的状态向量做concat,记为向量y

  • 如果有多层RNN,就把输出的向量y作为下一层RNN的输入
  • 如果只有一层RNN,就把y向量都丢掉,只保留两条RNN最后的状态向量,把它们concat,作为从输入文字中抽取的特征向量,以此来判断电影评论是正面还是负面

双向RNN总是比单向的效果好,原因:不管是SimpleRNN还是LSTM,都会或多或少遗忘掉早先的输入。而双向RNN左右结合,就不会遗忘一开始的词

python 复制代码
# 双向LSTM 用keras实现
from keras.models import Sequential
from keras.layers import LSTM, Embedding, Dense, Bidirectional

vocabulary = 10000   # unique words in the dictionary
embedding_dim = 32    # shape(x)  词向量x的维度
word_num = 500      # sequence length   每个评论长度为500个单词
state_dim = 32     # shape(h)   状态向量h的维度

model = Sequential()
model.add(Embedding(vocabulary, embedding_dim, input_length=word_num))   # 词映射为向量
# return_sequences=False  只保留两条链最后的状态,输出两个状态向量的concat,其余状态向量都被扔掉了
model.add(Bidirectional(LSTM(state_dim, return_sequences=False, dropout=0.2)))   # 指定状态向量h的维度 state_dim
model.add(Dense(1, activation='sigmoid'))   # 全连接层

model.summary()
  • Embedding层输出:(word_num, embedding_dim)
  • 双层RNN输出:(state_dim×2) 维的向量 return_sequences=False(输出两条链最后的状态向量)【参数数量比使用单向LSTM多一倍,因为两条链各自有各自的模型参数】

(3)pretrain(预训练)

比如在训练卷积神经网络时,如果网络太大,而训练集不够大,可以先在ImageNet等大数据上预训练,这样可以让神经网络比较好的初始化,也可以避免overfitting

若Embedding层参数>>训练样本数量,会导致overfitting。可以对Embedding层做预训练

  • 首先找一个更大的数据集(可以是情感分析数据或其他类型数据,但任务最好是接近情感分析任务,学出的词向量带正面或负面的情感。两个任务越相似,预训练后的transfer越好)
  • 搭建一个神经网络(有Embedding层即可),在大数据集上训练该神经网络
  • 训练完毕后,把上面的层全部丢掉,只保留Embedding层和训练好的模型参数
  • 再搭建自己的RNN网络(跟之前预训练的可以有不同的结构),新的RNN层和全连接层都是随机初始化,而Embedding层的参数是预训练出来的(固定住,不要训练)

总结循环神经网络RNN:

SimpleRNN和LSTM都属于RNN

(1)SimpleRNN很容易遗忘,效果不好,实践中不用

(2)LSTM的记忆比SimpleRNN长很多,实践中都用LSTM(还有GRU,但是效果不如LSTM)

(1)双向LSTM效果比单向好

(2)RNN层可以像全连接层和卷积层那样累加起来,搭成一个深度神经网络。多层RNN容量比单层RNN更大,如果训练数据够多,多层RNN效果更好

(3)想把RNN用在文本问题上,需要有一个Embedding层把词变成向量,Embedding层有一个参数矩阵(大小是vocabulary×词向量的维度)。这个参数矩阵通常很大,若训练数据集比较小,Embedding层就不会训练的很好,会overfitting。解决办法是在大数据集上预训练Embedding Layer

4、RNN的应用 --- 自动文本生成(Text Generation)

(1)技术原理

输入半句话,预测 input text 的下一个字符。拿训练好的RNN来生成文本:

  • 把文本分割成字符,用one-hot encoding来表示字符,这样,每个字符就表示成一个one-hot向量
  • 把这些one-hot向量依次输入RNN,RNN状态向量h会积累看到的信息。返回最后一个状态向量h
  • RNN后是一个softmax分类器,把h与参数矩阵W相乘,得到一个向量。经过softmax函数的变换,最终输出是一个向量,每个元素都在0-1之间,元素全加起来=1(softmax输出是一个概率分布)
  • 选择概率值最大的字符,接到文本末尾,作为新的输入,生成下一个字符,重复这个过程
如何训练这个RNN?
  • 训练数据:文本,如英文维基百科的所有文章。把文章划分成很多片段(可以有重叠overlap)
    • seg_len = 40(片段长度),stride = 3(下一个片段会向右平移3个字符的长度)
    • 片段是神经网络的输入,片段的下一个字符是标签,训练数据是 (片段, 标签) 的pairs

红色是片段,蓝色是标签

  • 多分类问题,每个类别对应一个概率值
  • 文本生成器并不是记住训练数据并重复,而是可以生成新的东西

(2)训练一个文本生成器

想要生成文本,首先需要训练一个RNN

  • 准备训练数据: 将训练文本划分为 (segment, next_char) 的 pairs。segment是神经网络的输入,next_char是标签

划分segment 和 next_char:

  • 字符->one-hot向量:故片段->矩阵 (之前还需要进一步做word embedding,用一个低维词向量来表示一个词。这里不需要embedding层,因为之前是word-level tokenization,英语里约有1w个常用词,one-hot向量都是1w维,维度太高;而char-level tokenization把一句话切成很多个字符,常用字符大概是100个=字母+数字+标点+空白)

len(text) = 600893 / stride = 3 约等于 200278 pairs

  • 搭建神经网络: 输入是segment(l×v的矩阵 ,l是每个segment的长度,即有l个字符;v是vocabulary,是字典里不同字符的数量)------> 单向LSTM (注意只能用单向LSTM,因为文本生成的下一个字符必须是从前往后)------>全连接层 (用softmax激活函数,多分类器)------> 输出v×1的向量(向量的每个元素是一个字符的概率)
  • **编译模型:**指定损失函数(CrossEntropy)和优化器(RMSProp),用训练数据拟合模型,训练几十个epochs
  • 训练好神经网络就可以生成文本,即预测下一个字符。首先需要给出seed segment,神经网络会接着你的输入生成文本。输出一个向量代表每个字符的概率值
  • 有了概率分布,如何生成下一个字符?三种方法:

|----------------------------------------------------------------------------------------------------|---------------------------------------------------|
| greedy selection,哪个字符的概率最大,就选择哪个字符 | 确定性的,没有随机性。给定初始的几个字符,后面生成的字符完全是确定的(完全取决于初始输入) |
| 从多项式分布中随机抽取。假如一个字符的概率值是0.3,那么它被选中的概率就是0.3 | 抽样过于随机,生成的文本会有很多拼写和语法错误 |
| 用介于0~1之间的temperature调整概率值:把概率值做幂变换,再归一化(**大的概率值会变大,小的会变小。**极端情况下,最大的概率值会变为1,其余都变为0,就相当于第一种确定性的选择) | 有随机性,但随机性不大,介于前两者之间。temperature越小,变换后的概率分布越极端 |

python 复制代码
# greedy selection
next_index = np.argmax(pred)

# sample from the multinomial distribution
next_onehot = np.random.multinomial(1, pred, 1)
next_index = np.argmax(next_onehot)

# adjust the multinomial distribution
pred = pred ** (1/temperature)   # controlled temperature
pred = pred/np.sum(pred)

temperature越小,变换后的概率分布越极端

神经网络怎样做文本生成?
  • 假设固定每个片段的长度为18个字符,最初的片段为seed(做one-hot变为矩阵),把矩阵输入神经网络,神经网络就会输出概率分布,从概率分布中抽样生成下一个字符
  • 把新生成的字符加到最后,作为下一轮的输入,输入的长度固定为18
  • 以此类推

文本生成是随机的,所以每次生成的都不一样

训练一个神经网络:

  • 将文本划分为 (segment, next_char) pairs
  • one-hot:
    • char ---> v×1 vector
    • segment ---> l×v matrix
  • 构建+训练神经网络:l×v矩阵 ---> LSTM ---> Dense ---> v×1 vector

文本生成:

  • 输入一个seed segment
  • 重复以下:
    • 将one-hot后的segment输入神经网络
    • 神经网络输出概率值
    • 从概率值中采样生成next_char
    • 将next_char append到segment后

5、RNN的应用 --- 机器翻译(Neural Machine Translation)

机器翻译模型有很多种,这里介绍Seq2Seq(例:英译德)

机器翻译是个Many to Many的问题,输入、输出长度都大于1且不固定

(1)处理数据

给定一句英语,如果翻译结果能match其中一个德语句子,就算翻译正确

  • 预处理:大写字母变为小写,去掉标点符号等
  • tokenization(可以是char-level或word-level,实际机器翻译都是word-level,因为数据量够大):要用两个不同的tokenizer (英语一个德语一个),并建立两个不同的字典(因为不同的语言通常有不同的字母表,且分词方法也不同)

例子里用的是char-level(比较方便,不用Embedding层),但最好用word-level(前提是需要有足够大的数据集)。原因:

  • 英文平均每个单词有4.5个字母,用单词代替字符,输入序列就会短4.5倍。序列越短,越不容易遗忘
  • word-level得到的Vocabulary大约为1w(也是one-hot的维度),必须要用word Embedding得到低维词向量(Embedding层参数数量太大,小数据量无法训练,会有overfitting的问题;或对Embedding层做预训练)
  • 英译德,故德语的字典里需要加入 起始符\t 和 终止符\n
  • 此时每句话都变成了一个字符的列表,并有一个英文字典和一个德语字典,再把每个字符映射为一个数字
  • 再把每个数字做one-hot,得到一个矩阵,这个矩阵就是RNN的输入

(2)训练Seq2Seq模型

Seq2Seq有一个Encoder编码器 (是个LSTM或其他RNN模型,用来从输入的句子中提取特征)和一个Decoder解码器(用来生成德语,就是文本生成器)

  • Encoder的最后一个状态就是从输入句子中提取的特征,包含这句话的信息。其余状态都被丢弃了。Encoder的输出是LSTM最后一个状态h 以及 最后的传输带c
  • Decoder跟文本生成器的区别是,文本生成器的初始状态是个全零向量,而Decoder初始状态是Encoder最后一个状态(从而得知输入的英语句子)
  • Decoder是一个LSTM,每次接收一个输入,输出对下一个字符的预测(输出一个概率分布向量p),第一个输入必须是起始符\t,将起始符后的第一个字符one-hot后作为label。损失函数为CrossEntropy
  • 最后一轮:整句德语作为Decoder输入,label为停止符\n

(3)用训练好的模型inference

最后一轮:

  • 每一轮会更新状态(h, c),并输出一个概率分布
  • 用新生成的字符作为下一轮的输入
  • 输出终止符\n时终止文本生成,并返回记录下的字符串,即模型翻译得到的德语

用Seq2Seq做机器翻译:

模型有一个Encoder (每输入一个词就更新状态,把输入信息积累在状态里。最后一个状态就是从英文句子里积累的特征。只保留最后一个状态 )和一个Decoder(Encoder的最后一个状态是Decoder的初始状态,初始化后Decoder就知道输入的英文句子了;然后Decoder就作为文本生成器,生成一句德语:首先把起始符\t作为Decoder RNN的输入,会更新状态为s1,全连接层输出预测概率为p1,根据概率分布做抽样生成下一个字符为z1;Decoder拿z1做输入,更新状态为s2,输出概率p2,得到新的字符z2,以此类推,直到输出停止符\n)

(4)怎么提升Seq2Seq?

Seq2Seq的原理是Encoder处理输入的英语句子,把信息都压缩到状态向量里,最后一个状态是整句话的概要(包含整句英语的完整信息)。但若英语句子很长,早期的输入就会被遗忘

四种改进方法:

  • Encoder用双向LSTM,但Decoder必须用单向(文本生成器必须按顺序生成文本)
  • 做word-level tokenization而不是char-level
  • Multi-task Learning(添加更多任务,等同于添加更多Decoder,注意Encoder只有一个):
    • 如添加一个Decoder把英语翻译为英语本身。这样一来,Encoder还是只有一个,但训练数据多了一倍,Encoder可以训练的更好
    • 或添加任务将英文翻译为其他语言。如用十种语言训练,Encoder的训练数据就多了十倍,可以训练的更好。即借助其他语言使Encoder变得更好

英语->英语 英语->其他语言

  • Attention

评估机器翻译的效果可以用BLEU(BiLingual Evaluation Understudy)指标,范围应该在0.1~0.5


(三)注意力

1、Attention(注意力机制)

(1)回顾Seq2Seq

有两个RNN网络,一个编码器Encoder(输入英语)和一个解码器Decoder(把英语翻译成德语)

  • Encoder每次读入一个英语词向量x,在状态h中积累输入的信息,最后一个状态hm中积累了所有词向量x的信息。Encoder输出最后一个状态hm,把之前的状态向量全都扔掉
  • Decoder初始状态s0=hm(包含了输入英语句子的信息),通过hm,Decoder就知道了这句英语。Decoder类似文本生成器,逐字生成一句德语(即模型生成的翻译)

**缺陷:**若输入句子很长,Encoder会记不住完整的句子,那么Decoder也就不可能产生正确的翻译

BLEU score是评价机器翻译好坏的标准,越高说明机器翻译越准确

(2)用Attention改进Seq2Seq

**解决Seq2Seq遗忘问题最有效的方法:**Attention(Decoder每次更新状态的时候,都会再看一遍Encoder所有状态,这样就不会遗忘;Attention还会告诉Decoder应该关注Encoder哪个状态)

Attention可以大幅提高准确率,但计算量较大

在Encoder结束工作后,Attention和Decoder同时开始工作
Encoder的所有状态都要保留,并计算s0与每个状态的相关性α(也叫权重,介于0~1,求和为1)

计算 hi 和 s0 的相关性,有2种方法:

(1)原论文提出:
tanh把每一个元素都压到 -1~1

(2)更常用,同Transformer:

  • 每一个 权重αi 对应一个 Encoder状态hi
  • 对 α 和 h 做加权平均,得到向量c(Context Vector)
  • 每一个 Context Vector ci 对应一个 Decoder状态si

c0是Encoder所有状态的加权平均,故c0知道Encoder输入x1~xm的完整信息;

Decoder新状态s1依赖于c0,故Decoder也知道Encoder的完整输入,解决了RNN遗忘的问题

Attention的时间复杂度(也是weights的数量):Encoder 和 Decoder 状态数量的乘积

可视化:

每当Decoder想要生成一个状态时,都会看一遍Encoder的所有状态,同时权重weights会告诉Decoder要关注Encoder的哪个状态

**Seq2Seq:**Decoder基于当前状态来生成下一个状态,这样产生的新状态可能已经忘了Encoder的部分输入

**Attention:**Decoder在产生下一个状态之前,会先看一遍Encoder的所有状态,于是Decoder就知道Encoder的完整信息,并不会遗忘;除此之外,还能告诉Decoder应该关注Encoder的哪个状态

Attention可以大幅提升Seq2Seq模型的表现,缺点是计算量太大

假设输入Encoder的序列长度为m,Decoder输出序列长度为t

Seq2Seq: 只需要Encoder读一遍输入序列,之后不会再看Encoder的输入或状态;Decoder依次生成输出序列,时间复杂度O(m+t)

Attention: Decoder每次更新状态,都要把Encoder的m个状态都看一遍,Decoder又有t个状态,故时间复杂度为O(mt)

2、Self-Attention

Attention用在Seq2Seq上,Seq2Seq有2个RNN网络(一个Encoder一个Decoder)

而Self-Attention是把Attention用在一个RNN网络上

SimpleRNN + Self-Attention

初始状态向量h0 和 Context Vector c0 都为全零向量

RNN读入第一个输入x1,需要更新状态h1:

计算新的Context Vector c1:是已有状态h的加权平均(由于初始状态h0是全零向量,故忽略h0,此时c1=h1)

计算新的状态h2:

计算新的Context Vector c2:

以此类推

初始状态向量h0 和 Context Vector c0 都为全零向量

重复以下步骤:

  • 读入向量xi
  • 用 xi 与 ci-1 计算出新的状态hi:hi = tanh(A·[xi ci-1]^T + b)
  • 拿当前状态hi与h1~hi(h0为全零向量,不考虑)作对比,计算权重α1~αi
  • 计算i个状态向量h的加权平均,得到新的context vector ci

RNN都有遗忘的问题,Self-Attention可以解决RNN遗忘的问题(每一轮更新状态之前,都会用Context Vector c看一遍之前所有的状态,这样就不会遗忘之前的信息了)

Self-Attention不局限于Seq2Seq模型,可以用在所有RNN上

除了避免遗忘,Self-Attention还能帮助RNN关注相关的信息
RNN从左往右读一句话,红色是当前输入,高亮是权重很大的位置(说明前文中最相关的词是什么)


(四)Transformer(=Attention without RNN)

1、剥离RNN,保留Attention

  • Transformer是一种Seq2Seq模型(Encoder & Decoder,适合做机器翻译)
  • Transformer不是循环神经网络RNN,没有循环的结构,只有Attention和全连接层
  • 在大数据集上,Transformer的accuracy显著高于RNN

(1)Attention for Seq2Seq Model

i是Encoder状态h的下标,j是Decoder状态s的下标

计算过程:

Attention中一共有3个参数矩阵:

Transformer里用的:

(2)Attention without RNN

Transformer就是由Attention层(Seq2Seq)和Self-Attention层组成的

一共有3个参数矩阵,Encoder中有K和V,Decoder中有Q

  • Encoder的input:x1~xm(生成Key和Value)
  • Decoder的input:x1'~xt'(生成Query)

如英译德,英语里有m个词变为词向量(即x1~xm),把当前生成的德语单词作为下一轮的输入:

Attention与RNN做机器翻译的不同在于:

  • RNN会把状态h作为特征向量输入softmax
  • 而Attention是把Context Vector c作为特征向量(可以用Attention Layer代替RNN,它不会遗忘)

Attention层:有两个输入序列X和X',有一个输出序列C,每个c向量对应一个x'向量

(3)Self-Attention without RNN

  • Attention用于Seq2Seq,有2个输入序列(如英译德,英文一个输入序列,德语一个输入序列)
  • Self-Attention不是Seq2Seq,它只有一个输入序列,其他跟Attention完全一样

以此类推计算得到其他α

以此类推计算得到其他c

ci 并非只依赖于 xi,而是依赖于所有m个x(改变任何一个x,输出的ci都会发生变化)

Attention最初提出是用在Seq2Seq模型,但Attention不局限于Seq2Seq,而是可以用在所有RNN上

若只有一个RNN网络,Attention就是Self-Attention

不用RNN,只用Attention,就是Transformer
用于Seq2Seq,可以做机器翻译,输入是两个序列 输入只有1个序列,输出的c向量类比于RNN输出的状态向量。Single-Head Self-Attention

(4)Multi-Head Self-Attention

由 l 个单头组成(不共享参数),每个单头有3个参数矩阵,故多头共有 3l 个参数矩阵

所有单头Self-Attention都有相同的输入x1~xm序列,但它们的参数矩阵各不相同,故输出的c序列也各不相同。把 l 个单头的输出(d×m)堆叠起来,作为多头的输出(ld×m)

(5)Multi-Head Attention

所有单头Attention的输入都是两个序列x1~xm以及x1'~xt'

每个单头Attention都有各自的参数矩阵(不共享参数)

每个单头都有自己的输出序列c,把单头输出的序列c堆叠起来,就是多头的输出

2、从Attention层到Transformer网络

(1)Stacked Self-Attention Layers

输入x1~xm,输出u1~um。但ui依赖于x1~xm,而不是仅仅依赖于xi

Transformer Encoder
  • 6个blocks,每个block有自己的参数,不共享
  • 每个Block有2层------Self-Attention Layer + Dense Layer
  • 输入和输出都是512×m的矩阵(m是输入序列x的长度,每个x向量都是512维),故可以用ResNet的Skip Connection方式,把输入加到输出上

(2)Stacked Attention Layers

Transformer Decoder
  • 一个block有3层:Self-Attention层、Attention层、全连接层
  • x1'~xt' 以及 c 以及 z 都是512维的向量

Decoder的一个Block如图所示,需要两个输入序列,输出一个序列

(3)Transformer

Encoder:

  • 6个Blocks,每个Block有2层
  • 输入有m列,每列都是512维的词向量,输出维度同输入

Decoder:

  • 6个Blocks,每个Block有3层:Self-Attention、Attention、全连接层
  • 每个Block有两个输入序列(Encoder网络的输出+上一个Decoder Block的输出),一个输出序列(t个向量,每个向量都是512维)

3、对比RNN Seq2Seq

两者输入、输出大小完全一样:

  • RNN Seq2Seq有两个输入序列(Encoder:x1~xm,Decoder:x1'~xt'),Transformer同
  • RNN Seq2Seq有一个输出序列s1~st(Decoder输出),Transformer同

m是输入序列的长度

4、Example:英译德

Encoder: 有6个block(block之间不共享参数,block之间还有skip-connection的技巧),每个block = 多头self-attention + dense,每个block的输入、输出都是512×m(m是输入序列的长度)

Decoder: 有6个block(block之间不共享参数,block之间还有skip-connection的技巧),每个block = 多头self-attention + 多头attention + dense,每个block的输入是两个序列:(512×m,512×t),输出一个序列512×t

Transformer:

  • Seq2Seq模型,有Encoder和Decoder,可以用来做机器翻译;
  • 不是RNN,无循环结构;
  • 完全基于Attention和Self-Attention和全连接层;
  • 和RNN的输入、输出大小一样

(五)Bert(Bidirectional Encoder Representations from Transformers)

一种用来预训练Transformer Encoder网络的方法,从而大幅提高准确率。有以下2个任务:

  • 随机遮挡一个或多个单词,让Encoder网络根据上下文来预测被遮挡的单词
  • 两个句子放在一起,让Encoder网络判断两句话是不是原文里相邻的两句话

1、预测被遮挡的单词

Um不仅依赖于Xm,而是依赖于所有X向量。即 Um在 [MASK] 位置上,但它包含整句话的上下文信息

用反向传播算出损失函数关于模型参数的梯度,然后做梯度下降来更新模型参数

  • Bert会随机遮挡单词,把遮住的单词作为标签;
  • Bert预训练不需要人工标注的数据集,可以自动生成标签
  • 多分类

2、预测下一个句子

二分类:0代表False(两句话不相邻),1代表True

例1:

例2:

50%是确实相邻的两句话(标签True),还有50%的第二句话是随机抽取的(标签False)

向量c在[CLS]位置上,但它包含两句话的全部信息,所以靠向量c就能判断两句话是否真实相邻

这样做预训练有什么用呢?

  • 相邻两句话通常有关联,这样做二分类可以强化这种关联,让Embedding和词向量包含这种关联
  • Encoder网络里有Self-Attention层(作用:找相关性),这种分类任务可以训练Self-Attention找到正确的相关性

3、结合两个任务

把两句话拼接起来,并随机遮挡15%的单词

目标函数是多个损失函数的加和。把目标函数关于模型参数求梯度,然后做梯度下降来更新模型参数

4、Bert的特点

  • 不需要人工标注数据,两种任务的标签都是自动生成的
  • 计算代价大
  • 训练好的模型参数是公开的

bert可以利用海量数据来训练一个超级大的模型

bert的Embedding层不是简单的word embedding,还有一些技巧


(六)ViT(Vision Transformer)

Dosovitskiy. An image is worth 16×16 words: transformers for image recognition at scale. In ICLR.

  • Transformer模型在图片分类(自动判断图片中的物体是什么)上的应用
  • 目前图片分类最好的模型,超越了最好的CNN(ResNet),前提是要在足够大的数据集上做预训练。数据集越大,ViT优势越明显

向量p是分类结果,p的每个元素对应一个类别,大小介于0~1,且相加为1

1、ViT

就是Transformer Encoder网络

(1)Split Image into Patches

把图片划分为大小相同的patches(可以有重叠overlap,也可以没有)

  • 用一个滑动窗口,每次移动若干个像素
  • 每次移动的步长叫stride(stride越小,得到的patches越多,计算量越大)

如划分为9块patches

对图片划分的时候,需要指定两个超参数:

  • patch size:每块patch的大小,如16×16
  • stride:滑动窗口移动的步长,如16×16(=patch size,无overlap)

(2)Vectorization向量化

每个小块都是一张彩色图片,有RGB三通道,即每个小块都是一个张量

把张量拉伸为向量:

设图片被划分为了n块,变为了n个向量,首先用全连接层对向量x做线性变换

  • 此处全连接层不使用激活函数,只是线性函数
  • W和b是参数,从训练数据中学习,且对全连接层是共享的

(3)Positional Encoding

对图片每一块的位置做编码

  • 图片被分为n块,那么位置就是1~n之间的整数,每个位置被编码为一个向量,向量大小跟z向量相同
  • 把位置编码向量加到z向量上,这样一来,一个z向量既是patch内容的表征,也包含patch的位置信息

为什么要用PE?

  • 如果不用,会掉3个百分点的准确率
  • 不同的PE方式表现几乎一样,用什么样的PE影响不大
  • 若z向量里不包含位置信息,那么以下两张图在Transformer眼里是一样的

(4)网络结构

  • x1~xn是图片中n个小块向量化得到的结果
  • 对x1~xn做线性变换,并加入位置信息,得到z1~zn为图片n个小块的表征(既包含内容信息,又包含位置信息)
  • [CLS] 表示分类,对这个符号做Embedding,得到向量z0(跟其他z向量大小相同),这个输出被用作分类
  • Transformer里有skip connection,把每一层的输入加到输出上
  • 还有BN的技巧

输出n+1个向量c,其中c1~cn没有用,c0可以看做是从图片中提取的特征向量,用作分类任务
向量p的大小为类别的数量

2、训练

随机初始化神经网络参数 -> 在大数据集A(如JFT,三亿张图片)上做预训练 -> 在小数据集B(任务/目标数据集,如ImageNet图片分类,30w张图片)训练集上做微调 -> 在数据集B测试集上评价模型表现,得到测试准确率

  • 当预训练数据集不够大时,ViT表现不好
  • 用越大的数据集(超过1亿张)做预训练,ViT效果越好(比ResNet高1个百分点)
  • 预训练数据量1亿或3亿,对于ResNet来说区别不大
相关推荐
爱喝白开水a35 分钟前
优化注意力层提升 Transformer 模型效率:通过改进注意力机制降低机器学习成本
人工智能·深度学习·机器学习·自然语言处理·大模型·transformer·大模型微调
z千鑫4 小时前
【人工智能】深入解析GPT、BERT与Transformer模型|从原理到应用的完整教程
人工智能·gpt·bert
xianghan收藏册12 小时前
提示学习(Prompting)篇
人工智能·深度学习·自然语言处理·chatgpt·transformer
L Jiawen20 小时前
【Python · PyTorch】循环神经网络 RNN(基础概念)
pytorch·python·rnn
weixin_543662861 天前
BERT的中文问答系统36-1
人工智能·python·bert
AI浩1 天前
上下文信息、全局信息、局部信息
人工智能·transformer
cv2016_DL1 天前
BERT相关知识
人工智能·算法·transformer
池央1 天前
深度学习模型:循环神经网络(RNN)
人工智能·rnn·深度学习
机器学习之心2 天前
聚划算!一区算法!双分解+牛顿拉夫逊优化+深度学习!CEEMDAN-VMD-NRBO-Transformer多元时序预测
深度学习·transformer·kmeans·ceemdan-vmd·nrbo·多元时序预测
狗窝超厉害2 天前
研0找实习【学nlp】15---我的后续,总结(暂时性完结)
人工智能·pytorch·python·自然语言处理·bert