如果我可以把今世的记忆带到以后,我会告诉我下一世的继任者去学数学。但是他可能又是一个不愿意学习的小傻瓜,或许三年级的时候还是会考各种0分。是呀,0分也是回忆,那时怎么会晓得走到现在,今后又晓得会去往何处。但是…,谁又会到全局最优解呢?我在这里,我不知道人生接下来会给我怎样的惊吓和惊喜,我现在处在的地方可能就是局部最优解吧!
所谓的序列标注就是对输入的文本序列中的每个元素打上标签集合中的标签。例如输入的一个序列如下:
X = x 1 , x 2 , . . . , x n X = {x_{1}, x_{2}, ..., x_{n}} X=x1,x2,...,xn
那么经过序列标注后每个元素对应的标签如下:
Y = y 1 , y 2 , . . . , y n Y = {y_{1}, y_{2}, ..., y_{n}} Y=y1,y2,...,yn
所以,其本质上是对线性序列中每个元素根据上下文内容进行分类的问题。一般情况下,对于NLP任务来说,线性序列就是输入的文本,往往可以把一个汉字看做线性序列的一个元素,而不同任务其标签集合代表的含义可能不太相同,但是相同的问题都是:如何根据汉字的上下文给汉字打上一个合适的标签(无论是分词,还是词性标注,或者是命名实体识别,道理都是想通的)
1.nlp中的词向量对比:word2vec/glove/fastText/elmo/GPT/bert
词向量是自然语言处理任务中非常重要的一个部分,词向量的表征能力很大程度上影响了自然语言处理模型的效果。如论文中所述,词向量需要解决两个问题:
(1). 词使用的复杂特性,如句法和语法。
(2). 如何在具体的语境下使用词,比如多义词的问题。
传统的词向量比如word2vec能够解决第一类问题,但是无法解决第二类问题。比如:“12号地铁线马上就要开通了,以后我们出行就更加方便了。”和“你什么时候方便,我们一起吃个饭。”这两个句子中的“方便”用word2vec学习到的词向量就无法区分,因为word2vec学习的是一个固定的词向量,它只能用同一个词向量来表示一个词不同的语义,而elmo就能处理这种多义词的问题。
缺点:稀疏;无序;纬度爆炸;每个向量都正交,相当于每个词都是没有关系的。
- word2vec解读
- 秒懂词向量Word2vec的本质
- Word2Vec详解(这个更详细)
- 小白都能理解的通俗易懂word2vec详解 (两个模型的推导过程通俗易懂)
在 NLP 中,把 x 看做一个句子里的一个词语,y 是这个词语的上下文词语,那么这里的 f,便是 NLP 中经常出现的语言模型(language model),这个模型的目的,就是判断 (x,y) 这个样本,是否符合自然语言的法则,更通俗点说就是:词语x和词语y放在一起,是不是人话。
Word2vec 正是来源于这个思想,但它的最终目的,不是要把 f 训练得多么完美,而是只关心模型训练完后的副产物——模型参数(这里特指神经网络的权重),并将这些参数,作为输入 x 的某种向量化的表示,这个向量便叫做——词向量。
CBoW模型等价于一个词袋模型的向量乘以一个Embedding矩阵,从而得到一个连续的embedding向量。
CBoW前向计算过程
词向量最简单的方式是one-hot方式。one-hot就是从很大的词库corpus里选V个频率最高的词(忽略其他的) ,V一般比较大,比如V=10W,固定这些词的顺序,然后每个词就可以用一个V维的稀疏向量表示了,这个向量只有一个位置的元素是1,其他位置的元素都是0。在上图中,
具体计算过程
因为权值矩阵是一个非常大的矩阵,比如词典是10000,期望的词向量维度是300,那么这个矩阵就有300万参数,而这对于最后一层的softmax和反向传播都会带来极低的效率。因此以下两个技巧都是为了提升模型的速度。
1. 层softmax技巧(hierarchical softmax)
解释一: 最后预测输出向量时候,大小是1*V的向量,本质上是个多分类的问题。通过hierarchical softmax的技巧,把V分类的问题变成了log(V)次二分类。
解释二: 层次softmax的技巧是来对需要训练的参数的数目进行降低。所谓层次softmax实际上是在构建一个哈夫曼树,这里的哈夫曼树具体来说就是对于词频较高的词汇,它的树的深度就较浅,对于词频较低的单词的它的树深度就较大。
总结: 层次softmax就是利用一颗哈夫曼树来简化原来的softmax的计算量。具体来说就是对词频较高的单词,他在哈夫曼树上的位置就比较浅,而词频较低的位置就在树上的位置比较深。
2. 负采样(negative sampling)
解释一: 本质上是对训练集进行了采样,从而减小了训练集的大小。每个词的概率由下式决定:
l e n ( w ) = c o u n t ( w ) 3 / 4 ∑ u ∈ v o c a b c o u n t ( u ) 3 / 4 len(w) = \frac{count(w)^{3/4}}{\sum\limits_{u \in vocab} count(u)^{3/4}} len(w)=u∈vocab∑count(u)3/4count(w)3/4
在训练每个样本时, 原始神经网络隐藏层权重的每次都会更新, 而负采样只挑选部分权重做小范围更新
解释二:
负采样主要解决的问题就是参数量过大,模型很难训练的问题。那么什么是负采样中的正例和负例?如果 vocabulary 大小为1万时, 当输入样本 ( “fox”, “quick”) 到神经网络时, “ fox” 经过 one-hot 编码,在输出层我们期望对应 “quick” 单词的那个神经元结点输出 1(这就是正例),其余 9999 个都应该输出 0(这就是负例)。在这里,这9999个我们期望输出为0的神经元结点所对应的单词我们称为 negative word. negative sampling 的想法也很直接 ,将随机选择一小部分的 negative words,比如选 10个 negative words 来更新对应的权重参数。
解释三:
Negative Sampling是对于给定的词,并生成其负采样词集合的一种策略,已知有一个词,这个词可以看做一个正例,而它的上下文词集可以看做是负例,但是负例的样本太多,而在语料库中,各个词出现的频率是不一样的,所以在采样时可以要求高频词选中的概率较大,低频词选中的概率较小,这样就转化为一个带权采样问题,大幅度提高了模型的性能。
在cbow方法中,是用周围词预测中心词,从而利用中心词的预测结果情况,使用Gradient Desent方法,不断的去调整周围词的向量。当训练完成之后,每个词都会作为中心词,把周围词的词向量进行了调整,这样也就获得了整个文本里面所有词的词向量。要注意的是, cbow的对周围词的调整是统一的:求出的gradient的值会同样的作用到每个周围词的词向量当中去。因此,cbow预测行为的次数跟整个文本的词数几乎是相等的(每次预测行为才会进行一次back propgation, 而往往这也是最耗时的部分),复杂度大概是O(V)(每个单词的词向量调整V次)。
而skip-gram是用中心词来预测周围的词。在skip-gram中,会利用周围的词的预测结果情况,使用Gradient Decent来不断的调整中心词的词向量,最终所有的文本遍历完毕之后,也就得到了文本所有词的词向量。可以看出,skip-gram进行预测的次数是要多于cbow的:因为每个词在作为中心词时,都要使用周围词进行预测一次。这样相当于比cbow的方法多进行了K次(假设K为窗口大小),因此时间的复杂度为O(KV)(每个单词的词向量调整的次数是KV次,K是窗口的大小,V是语料库中单词的数量),训练时间要比cbow要长。
但是在skip-gram当中,每个词都要收到周围的词的影响,每个词在作为中心词的时候,都要进行K次的预测、调整。因此, 当数据量较少,或者词为生僻词出现次数较少时, 这种多次的调整(skip-gram的训练方法)会使得词向量相对的更加准确。因为尽管cbow从另外一个角度来说,某个词也是会受到多次周围词的影响(多次将其包含在内的窗口移动),进行词向量的跳帧,但是他的调整是跟周围的词一起调整的,grad的值会平均分到该词上, 相当于该生僻词没有收到专门的训练,它只是沾了周围词的光而已。
因此,从更通俗的角度来说:
在skip-gram里面,每个词在作为中心词的时候,实际上是 1个学生 VS K个老师,K个老师(周围词)都会对学生(中心词)进行“专业”的训练,这样学生(中心词)的“能力”(向量结果)相对就会扎实(准确)一些,但是这样肯定会使用更长的时间;
cbow是 1个老师 VS K个学生,K个学生(周围词)都会从老师(中心词)那里学习知识,但是老师(中心词)是一视同仁的,教给大家的一样的知识。至于你学到了多少,还要看下一轮(假如还在窗口内),或者以后的某一轮,你还有机会加入老师的课堂当中(再次出现作为周围词),跟着大家一起学习,然后进步一点。因此相对skip-gram,你的业务能力肯定没有人家强,但是对于整个训练营(训练过程)来说,这样肯定效率高,速度更快。
一句话:word2vec就是一系列的模型的权重。利用一个有监督方式来训练模型,利用模型中得到的权重来表示一个词。
另一种解释:word2vec是用一个一层的神经网络 (即CBOW) 把one-hot形式的稀疏词向量映射称为一个n维(n一般为几百)的稠密向量的过程。为了加快模型训练速度,其中的tricks包括Hierarchical softmax,negative sampling, Huffman Tree等。
Q1: word2vec是如何解决oov问题的?
word2vec并没有解决oov问题。但是后续有很多解决的办法,例如
Q2. 为什么要去除停用词
文档中如果大量使用Stop words容易对页面中的有效信息造成噪音干扰,所以适当地减少停用词出现的频率,可以有效地帮助我们提高关键词密度,让关键词更集中、更突出。
Q3. Negative Sampling是如何做的
Negative Sampling是对于给定的词,并生成其负采样词集合的一种策略,已知有一个词,这个词可以看做一个正例,而它的上下文词集可以看做是负例,但是负例的样本太多,而在语料库中,各个词出现的频率是不一样的,所以在采样时可以要求高频词选中的概率较大,低频词选中的概率较小,这样就转化为一个带权采样问题,大幅度提高了模型的性能。
Q4. word2vec是如何训练的
见上面
Q5. 针对生僻词,哪种训练方法更合适?
skip-gram模型更合适
当数据量较少,或者词为生僻词出现次数较少时, 这种多次的调整会使得词向量相对的更加准确。因为尽管cbow从另外一个角度来说,某个词也是会受到多次周围词的影响(多次将其包含在内的窗口移动),进行词向量的跳帧,但是他的调整是跟周围的词一起调整的,反向传播梯度的值会平均分到该词上, 相当于该生僻词没有收到专门的训练,它只是沾了周围词的光而已。
参考博文:
- CSDN上的一个回答,讲解的比较全面
- 对损失函数做了一个简单的分析,可以作为对上面回答的一个补充
Glove融合了矩阵分解和全局统计信息的优势,统计语料库的词-词之间的共现矩阵,加快模型的训练速度而且又可以控制词的相对权重。
GloVe的全称叫Global Vectors for Word Representation,它是一个基于全局词频统计(count-based & overall statistics)的词表征(word representation)工具,它可以把一个单词表达成一个由实数组成的向量,这些向量捕捉到了单词之间一些语义特性,比如相似性(similarity)、类比性(analogy)等。我们通过对向量的运算,比如欧几里得距离或者cos相似度,可以计算出两个单词之间的语义相似性。
开始 -> 统计共现矩阵 -> 训练词向量 -> 结束
设共现矩阵为X,其元素为Xi,j.
Xi,j 的意义为:在整个语料库中,单词i和单词 j 共同出现在一个窗口中的次数。
例子:
i love you but you love him i am sad
这个小小的语料库只有1个句子,涉及到7个单词:i、love、you、but、him、am、sad。
如果我们采用一个窗口宽度为5(左右长度都为2)的统计窗口,那么就有以下窗口内容:
窗口标号 | 中心词 | 窗口内容 |
---|---|---|
0 | i | i love you |
1 | love | i love you but |
2 | you | i love you but you |
3 | but | love you but you love |
4 | you | you but you love him |
5 | love | but you love him i |
6 | him | you love him i am |
7 | i | love him i am sad |
8 | am | him i am sad |
9 | sad | i am sad |
窗口0、1长度小于5是因为中心词左侧内容少于2个,同理窗口8、9长度也小于5。
以窗口5为例说明如何构造共现矩阵:
2.1 共现矩阵的一个更直观例子
Corpus:
1. I like deep learning.
2. I like NLP.
3. I enjoy flying.
从语料库包含的单词为:I、 like、 deep、 learning、 NLP、 enjoy、 flying. 假设window length 为1. 那么这个语料库的共现矩阵便如下图所示:
共现矩阵存在的问题:
为了解决维度过大的问题,Glove的提出者采用一个特殊的方法(由于数学知识有限,这个方法并没有理解,所以暂且忽略这个降维的过程)进行降维。
J = ∑ i , j N f ( X i , j ) ( v i T v j + b i + b j − l o g ( X i , j ) ) 2 J=\sum_{i,j}^Nf(X_{i,j})(v_{i}^Tv_{j}+b_{i}+b_{j}-log(X_{i,j}))^2 J=i,j∑Nf(Xi,j)(viTvj+bi+bj−log(Xi,j))2
v i , v j v_i,v_j vi,vj 是单词i和单词j的词向量, b i , b j b_i,b_j bi,bj 是两个标量(作者定义的偏差项),f是权重函数,N是词汇表的大小(共现矩阵维度为N*N), X i , j X_{i,j} Xi,j代表共现矩阵中由 i 和 j 定位到的数值。
GloVe是一种无监督(unsupervised learing)的学习方式(因为它确实不需要人工标注label),但其实它还是有label的,这个label就是以上公式中的 l o g ( X i j ) log(X_{ij}) log(Xij),而公式中的向量 v i , v j v_{i},v_{j} vi,vj就是要不断更新学习的参数,所以本质上它的训练方式跟监督学习的训练方法没什么不一样,都是基于梯度下降的。
具体地,论文里的实验是这么做的:采用了AdaGrad的梯度下降算法,对矩阵 X 中的所有非零元素进行随机采样,学习曲率(learning rate)设为0.05,在vector size小于300的情况下迭代了50次,其他大小的vectors上迭代了100次,直至收敛。最终学习得到的是两个vector是 v i , v j v_{i}, v_{j} vi,vj ,因为 X 是对称的(symmetric),所以从原理上讲 v i , v j v_{i}, v_{j} vi,vj 是也是对称的,他们唯一的区别是初始化的值不一样,而导致最终的值不一样。
LSA(Latent Semantic Analysis)是一种比较早的count-based的词向量表征工具,它也是基于co-occurance matrix的,只不过采用了基于奇异值分解(SVD)的矩阵分解技术对大矩阵进行降维,而SVD的复杂度是很高的,所以它的计算代价比较大。还有一点是它对所有单词的统计权重都是一致的。而这些缺点在GloVe中被一一克服了。
而word2vec最大的缺点则是没有充分利用所有的语料,所以GloVe其实是把两者的优点结合了起来。从这篇论文给出的实验结果来看,GloVe的性能是远超LSA和word2vec的,但网上也有人说GloVe和word2vec实际表现其实差不多。
Q1: 未训练之前的词向量是用什么进行表示的
比如说要训练维度300的词向量,未训练之前就是随机初始化的N个300维的词向量。
Q2. Glove和skip-gram、CBOW模型对比
- ELMo超详细解读
- ELMo论文解读——原理、结构及应用
- ELMo的使用
- 史上最全词向量讲解(LSA/word2vec/Glove/FastText/ELMo/BERT)
- ELMo原理解析及简单上手使用
- 李宏毅的视频,对于训练的过程和使用的过程讲的很清楚
word2vec和Glove词向量表征的缺点是对于每一个单词都有唯一的一个embedding表示, 也就是在训练完成以后,一个文本中相同的token就具有相同的词向量表征,而对于多义词显然这种做法不符合直觉, 但是单词的意思又和上下文相关, ELMo的做法是我们只预训练language model,而word embedding是通过输入的句子实时输出的, 这样单词的意思就是上下文相关的了,这样就很大程度上缓解了歧义的发生.
在此之前的 Word Embedding 本质上是个静态的方式,所谓静态指的是训练好之后每个单词的表达就固定住了,以后使用的时候,不论新句子上下文单词是什么,这个单词的 Word Embedding 不会跟着上下文场景的变化而改变,所以对于比如 Bank 这个词,它事先学好的 Word Embedding 中混合了几种语义,在应用中来了个新句子,即使从上下文中(比如句子包含 money 等词)明显可以看出它代表的是「银行」的含义,但是对应的 Word Embedding 内容也不会变,它还是混合了多种语义。这是为何说它是静态的,这也是问题所在。
ELMO 本身是个根据当前上下文对 Word Embedding 动态调整的思路
word2vec和glove是固定好的词向量,一个语料库中同样的单词只能对应一个embedding,其包含了各种的语义关系,这对于多义词是不友好的。而ELMo是动态的产生一句话中所有token对应的词向量,ELMo的输入就是一句话,得到每句话中每个token对应的embedding,因此相同的token可能有不同的embedding。
Elmo主要使用了一个两层双向的LSTM语言模型
左边前向LSTM中输入的是句子的上文,右边后向LSTM中输入的是句子的下文。训练好之后以三层内部状态的函数来表示词向量。
最终模型输入的是128个句子(一批)正反向512维的词向量,词向量经过字符卷积得到,每个句子截断为20个词,不足的补齐。
以"I love China very much"为例,如果要预测的 T 1 T1 T1 是love,那么 E 1 E1 E1 处输入的就是"I"和"China".
4.1 目的
解决深度神经网络中训练困难的问题。
4.2 原理
使用门控单元,使输入以一定的比例穿过网络,增加网络的灵活性。
最后的词向量是三个词向量的结合:初始化词向量,第一层BiLSTM的输出,第二层BiLSTM的输出,三者加权求和得到最后的词向量。这三层词向量都被scale到了1024维。
1.为什么使用三层的输出加权求和得到最后的词向量?
这里之所以将3层的输出都作为token的embedding表示是因为实验已经证实不同层的LM输出的信息对于不同的任务作用是不同的, 也就是所不同层的输出捕捉到的token的信息是不相同的。
- 一些关于BERT的问题整理记录
- 一些关于Transformer问题整理记录
- 一些关于ELMo问题整理记录
- 知乎上相当漂亮的一个回答
- BERT大火却不懂Transformer?读这一篇就够了
- 理解Transformer的三层境界–Attention is all you need
- 李宏毅的视频,对于训练的过程和使用的过程讲的很清楚
Z = s o f t m a x ( Q T K d k ) V Z = softmax(\frac{Q^TK}{\sqrt{d_{k}}})V Z=softmax(dkQTK)V
其中的 d k d_{k} dk代表的是 K K K的维度,原文中是64.
X矩阵中的每一行代表一个单词的词向量,其中的 W Q , W K , W V W^Q, W^K, W^V WQ,WK,WV都是随机初始化的权重矩阵
Multi-headed 的机制是为了完善self-attention的性能提出来的。他的能力主要体现在两个方面:
Transformer中的位置编码是由cos/sin位置函数产生的,BERT是随机产生的。由于BERT的语料库非常大并且参数量非常大,这就使得随机位置向量可以得到学习。最后实验表明这和Transformer中的cos/sin位置函数生成的位置向量效果相同。
残差网络主要是链接两个self-attention层。他就是把单元的输入直接与单元输出加在一起,然后再做下游的任务。
在Transformer中的应用如下
Transformer本身是一个encoder-decoder模型,那么也就可以从encoder和decoder两个方面介绍。
对于encoder,原文是由6个相同的大模块组成,每一个大模块内部又包含多头self-attention模块和前馈神经网络模块。尤其是对于多头self-attention模块相对与传统的attention机制能更关注输入序列中单词与单词之间的依赖关系,让输入序列的表达更加丰富。同时这里的encoder模块也是BERT的一个主要的组成模块。
对于decoder模块,原文中也是包含了6个相同的大模块,每一个大模块由self-attention模块,encoder-decoder交互模块以及前馈神经网络模块三个子模块组成。其self-attention模块和前馈神经网络模块和encoder端是一致的;对于encoder和decoder模块有点类似于传统的attention机制,目的就在于让Decoder端的单词(token)给予Encoder端对应的单词(token)“更多的关注(attention weight)”。
注意decoder中的K和V来自于encoder
Q1. Transformer Decoder端的输入具体是什么
对于第一个大模块,简而言之,其训练及测试时接收的输入为:
训练的时候每次的输入为上次的输入加上输入序列向后移一位的ground truth (例如每向后移一位就是一个新的单词,那么则加上其对应的embedding),特别地,当decoder的time step为1时(也就是第一次接收输入),其输入为一个特殊的token,可能是目标序列开始的token(如),也可能是源序列结尾的token(如),也可能是其它视任务而定的输入等等,不同源码中可能有微小的差异,其目标则是预测下一个位置的单词(token)是什么,对应到time step为1时,则是预测目标序列的第一个单词(token)是什么,以此类推;
这里需要注意的是,在实际实现中可能不会这样每次动态的输入,而是一次性把目标序列的embedding通通输入第一个大模块中,然后在多头attention模块对序列进行mask即可,
这里为什么要进行mask,因为我们需要对后面的序列进行预测,在训练的时候,后面的序列信息对前面是不可知的,所以需要把后面的序列进行sequence mask。
在测试的时候,是先生成第一个位置的输出,然后有了这个之后,第二次预测时,再将其加入输入序列,以此类推直至预测结束。
简而言之:1)训练时每次的输入是上一时刻输出加上输入序列的后移一位的ground truth;2)测试时是先生成第一个位置的输出,然后有了这个以后,第二次预测时再将其加入到输入序列,以此类推直至预测结束。
Q2. 什么是sequence mask
sequence mask 是为了使得 decoder 不能看见未来的信息。也就是对于一个序列,在 time_step 为 t 的时刻,我们的解码输出应该只能依赖于 t 时刻之前的输出,而不能依赖 t 之后的输出。因此我们需要想一个办法,把 t 之后的信息给隐藏起来。
Q3. encoder和decoder端的self-attention有什么不同
Decoder端的多头self-attention需要做mask,因为它在预测时,是“看不到未来的序列的”,所以要将当前预测的单词(token)及其之后的单词(token)全部mask掉。
Q4. self-attention为什么有用
self-attention可以捕获同一个句子中单词之间的一些句法特征或者语义特征。引入Self Attention后会更容易捕获句子中长距离的相互依赖的特征,如果是RNN或者LSTM,需要依次序序列计算,对于远距离的相互依赖的特征,要经过若干时间步步骤的信息累积才能将两者联系起来,而距离越远,有效捕获的可能性越小。但是Self Attention在计算过程中会直接将句子中任意两个单词的联系通过一个计算步骤直接联系起来,所以远距离依赖特征之间的距离被极大缩短,有利于有效地利用这些特征。
Q5. 为什么需要multi-head attention
因为只是用一个self-attention,当前单词可能会起一个主导作用,其他文本信息会被削弱。multi-head attention会形成多个子空间,扩展了模型专注于不同位置的能力,然后再将各方面的信息综合起来,着有助于网络捕捉到更丰富文本特征。
Q6. Transformer相对于seq2seq的优点
Q7. Transformer是如何并行计算的
其并行能力主要体现在self-attention模块,因为对与某个序列 x 1 , x 2 , . . . x n x_1,x_2,...x_n x1,x2,...xn,self-attention可以直接计算 x i , x j x_i,x_j xi,xj的结果,而RNN模型必须按照顺序计算。
Q8. Transformer中句子的encoder表示是什么?如何加入词序信息的?
Encoder端得到的是整个输入序列的encoding表示,其中最重要的是经过了self-attention模块,让输入序列的表达更加丰富,而加入词序信息是使用不同频率的正弦和余弦函数。
Q9. Transformer中的FFNN两种构成方式的区别
一种是传统的DNN,一种是CNN(CNN也是前馈神经网络)
Q10. layer nomalization的before和after的区别(其实就是nomarlization层需放在激活函数前面还是后面的问题)
Pre-LN相较传统Transformer的Post-LN在训练阶段可以不需要warm-up并且收敛更快。
- 一些关于BERT的问题整理记录
- 一些关于Transformer问题整理记录
- 一些关于ELMo问题整理记录
- BERT原理介绍
- 李宏毅的视频,对于训练的过程和使用的过程讲的很清楚
什么是预训练模型
首先我们要了解一下什么是预训练模型,举个例子,假设我们有大量的维基百科数据,那么我们可以用这部分巨大的数据来训练一个泛化能力很强的模型,当我们需要在特定场景使用时,例如做文本相似度计算,那么,只需要简单的修改一些输出层,再用我们自己的数据进行一个增量训练,对权重进行一个轻微的调整。
预训练的好处在于在特定场景使用时不需要用大量的语料来进行训练,节约时间效率高效,bert就是这样的一个泛化能力较强的预训练模型,也就是得到模型参数,然后实时输出句子中每个token的embedding,类似于ELMo;
5.2.1 结构
BERT的内部结构,官网提供了两个版本,L表示的是transformer的层数,H表示输出的维度,A表示mutil-head attention的个数
B E R T B A S E : L = 12 , H = 768 , A = 12 , T o t a l P a r a m e t e r s = 110 M BERT_{BASE}:L=12,H=768,A=12,Total Parameters=110M BERTBASE:L=12,H=768,A=12,TotalParameters=110M
B E R T L A R G E : L = 24 , H = 1024 , A = 16 , T o t a l P a r a m e t e r s = 340 M BERT_{LARGE}:L=24, H=1024, A=16, Total Parameters=340M BERTLARGE:L=24,H=1024,A=16,TotalParameters=340M
从模型的层数来说其实已经很大了,但是由于transformer的residual模块,层数并不会引起梯度消失等问题,但是并不代表层数越多效果越好,有论点认为低层偏向于语法、句法特征学习,高层偏向于语义特征学习,因此使用的时候可以适当调整BERT的层数。
5.2.2 BERT的训练过程
BERT的预训练阶段采用了两个独有的非监督任务,一个是Masked Language Model,还有一个是Next Sentence Prediction。
- 第一个任务是采用MaskLM的方式来训练语言模型,通俗地说就是在输入一句话的时候,随机地选一些要预测的词,然后用一个特殊的符号[MASK]来代替它们,之后让模型根据所给的标签去学习这些地方该填的词。
- 第二个任务在双向语言模型的基础上额外增加了一个句子级别的连续性预测任务,即预测输入BERT的两段文本是否为连续的文本.
1) Masked Language Model
mlm可以理解为完形填空,作者会随机mask每一个句子中15%的词,用其上下文来做预测,例如:my dog is hairy → my dog is [MASK]
此处将hairy进行了mask处理,然后采用非监督学习的方法预测mask位置的词是什么,但是该方法有一个问题,因为是mask15%的词,其数量已经很高了,这样就会导致某些词在fine-tuning阶段从未见过,为了解决这个问题,作者做了如下的处理:
那么为啥要以一定的概率使用随机词呢?这是因为transformer要保持对每个输入token分布式的表征,否则Transformer很可能会记住这个[MASK]就是"hairy"。至于使用随机词带来的负面影响,文章中说了,所有其他的token(即非"hairy"的token)共享15%*10% = 1.5%的概率,其影响是可以忽略不计的。
2)Next Sentence Prediction
选择一些句子对A与B,其中50%的数据B是A的下一条句子,剩余50%的数据B是语料库中随机选择的,学习其中的相关性,添加这样的预训练的目的是目前很多NLP的任务比如QA和NLI都需要理解两个句子之间的关系,从而能让预训练的模型更好的适应这样的任务。
5.2.3 模型的输入和输出
BERT的输入可以是单一的一个句子或者是句子对,实际的输入值包括了三个部分,分别是token embedding词向量,segment embedding句向量,每个句子有个句子整体的embedding项对应给每个单词,还有position embedding位置向量,这三个部分相加形成了最终的bert输入向量。
BERT的损失函数由两部分组成,第一部分是来自 Mask-LM 的单词级别分类任务,另一部分是句子级别的分类任务。通过这两个任务的联合学习,可以使得 BERT 学习到的表征既有 token 级别信息,同时也包含了句子级别的语义信息。具体损失函数如下:
L ( θ , θ 1 , θ 2 ) = L 1 ( θ , θ 1 ) + L 2 ( θ , θ 2 ) L(\theta, \theta_{1}, \theta_{2}) = L_{1}(\theta, \theta_1)+L_{2}(\theta, \theta_{2}) L(θ,θ1,θ2)=L1(θ,θ1)+L2(θ,θ2)
其中 θ \theta θ 是BERT中Encoder部分的参数, θ 1 \theta_1 θ1 是 Mask-LM 任务中在 Encoder 上所接的输出层中的参数, θ 2 \theta_2 θ2则是句子预测任务中在 Encoder 接上的分类器参数。因此,在第一部分的损失函数中,如果被 mask 的词集合为 M,因为它是一个词典大小 |V| 上的多分类问题,那么具体说来有:
L 1 ( θ , θ 1 ) = − ∑ i = 1 M l o g p ( m = m i ∣ θ , θ 1 ) , m i ∈ [ 1 , 2 , . . . , ∣ V ∣ ] L_{1}(\theta,\theta_{1}) = -\sum{_{i=1}^M}log p(m=m_{i}|\theta,\theta_{1}), m_{i} \in [1, 2,...,|V|] L1(θ,θ1)=−∑i=1Mlogp(m=mi∣θ,θ1),mi∈[1,2,...,∣V∣]
在句子中预测任务中,也是一个分类问题的损失函数:
L 2 ( θ , θ 2 ) = − ∑ j = 1 N l o g p ( n = n j ∣ θ , θ 2 ) , n i [ I s N e x t , N o t N e x t ] L_{2}(\theta,\theta_{2}) = -\sum{_{j=1}^N}log p(n=n_{j}|\theta,\theta_{2}), n_{i}[IsNext, NotNext] L2(θ,θ2)=−∑j=1Nlogp(n=nj∣θ,θ2),ni[IsNext,NotNext]
L ( θ , θ 1 , θ 2 ) = − ∑ i = 1 M l o g p ( m = m i ∣ θ , θ 1 ) − ∑ j = 1 N l o g p ( n = n j ∣ θ , θ 2 ) L(\theta, \theta_{1}, \theta_{2}) = -\sum{_{i=1}^M}log p(m=m_{i}|\theta,\theta_{1})-\sum{_{j=1}^N}log p(n=n_{j}|\theta,\theta_{2}) L(θ,θ1,θ2)=−∑i=1Mlogp(m=mi∣θ,θ1)−∑j=1Nlogp(n=nj∣θ,θ2)
Q1. 为什么BERT的效果比ELMo好,两者有什么区别?
ELMo模型是通过语言模型任务得到句子中单词的embedding表示,以此作为补充的新特征给下游任务使用。因为ELMO给下游提供的是每个单词的特征形式,所以这一类预训练的方法被称为“Feature-based Pre-Training”。而BERT模型是“基于Fine-tuning的模式”,这种做法和图像领域基于Fine-tuning的方式基本一致,下游任务需要将模型改造成BERT模型,才可利用BERT模型预训练好的参数。
Q2. BERT模型为什么要用mask?
BERT通过在输入X中随机Mask掉一部分单词,然后预训练过程的主要任务之一是根据上下文单词来预测这些被Mask掉的单词,那些被Mask掉的单词就是在输入侧加入的所谓噪音。类似BERT这种预训练模式,被称为DAE LM。因此总结来说BERT模型 [Mask] 标记就是引入噪音的手段。
关于DAE LM预训练模式,优点是它能比较自然地融入双向语言模型,同时看到被预测单词的上文和下文,然而缺点也很明显,主要在输入侧引入 [Mask] 标记,导致预训练阶段和Fine-tuning阶段不一致的问题。
另一种解释:
这样相当于添加一个噪声,预测一个词汇时,模型并不知道输入对应位置的词汇是否为正确的词汇( 10% 概率),这就迫使模型更多地依赖于上下文信息去预测词汇,并且赋予了模型一定的纠错能力。
Q3. mask和CBOW不一致的地方
相同点:
不同点:
首先,在CBOW中,每个单词都会成为input word,而BERT不是这么做的,原因是这样做的话,训练数据就太大了,而且训练时间也会非常长。
其次,对于输入数据部分,CBOW中的输入数据只有待预测单词的上下文,而BERT的输入是带有[MASK] token的“完整”句子,也就是说BERT在输入端将待预测的input word用[MASK] token代替了。
另外,通过CBOW模型训练后,每个单词的word embedding是唯一的,因此并不能很好的处理一词多义的问题,而BERT模型得到的word embedding(token embedding)融合了上下文的信息,就算是同一个单词,在不同的上下文环境下,得到的word embedding是不一样的。
Q4. BERT的embedding向量如何的来的
以中文为例,BERT模型通过查询字向量表将文本中的每个字转换为一维向量,作为模型输入(还有position embedding和segment embedding);模型输出则是输入各字对应的融合全文语义信息后的向量表示。
而对于输入的token embedding、segment embedding、position embedding都是随机生成的,需要注意的是在Transformer论文中的position embedding由sin/cos函数生成的固定的值,而在这里代码实现中是跟普通 word embedding 一样随机生成的,可以训练的。**作者这里这样选择的原因可能是BERT训练的数据比Transformer那篇大很多,完全可以让模型自己去学习 **(这就是为什么BERT的位置向量是随机生成的)。
Q5. multi-head attention的具体结构
这里面Multi-head Attention其实就是多个Self-Attention结构的结合,每个head学习到在不同表示空间中的特征,如下图所示,两个head学习到的Attention侧重点可能略有不同,这样给了模型更大的容量。
针对一个具体的输入,初始化多个QKV矩阵,再BERT-large当中是12个,然后相当于做12次的self-attention,把最后的结果进行拼接,得到一个768维度的输出,然后通过一个权重矩阵 W 0 W^0 W0 进行处理得到最后的multi-head attention层的输出。
Q6. Bert 采用哪种Normalization结构,LayerNorm和BatchNorm区别,LayerNorm结构有参数吗,参数的作用
BN和LN的区别
- Batch Normalization 的处理对象是对一批样本, Layer Normalization 的处理对象是单个样本。
- Batch Normalization 是对这批样本的同一维度特征做归一化, Layer Normalization 是对这单个样本的所有维度特征做归一化。
- BN是纵向的做normalization,LN是横向的做normalization
Q7. transformer attention的时候为什么要除以 d k \sqrt{d_{k}} dk
至于attention后的权重为啥要除以 ,作者在论文中的解释是点积后的结果大小是跟维度成正比的,所以经过softmax以后,梯度就会变很小,除以 后可以让attention的权重分布方差为1,而不是 。
Q8. wordpice的作用
主要就是为了降低OOV的情况
Q9. 如何优化BERT的效果
Q10. 如何优化BERT的性能
Q11. Bert的位置embedding为什么要随机初始化,而不是sin/cos得到
由于BERT训练的语料库非常大,并且参数比较多,通过经验公式计算和随机初始化让网络学习到的位置编码效果几乎一样,而随机初始化更方便,所以使用随机初始化。
Q12. BERT为什么好用
Q13. BERT得一些缺点
GPT和BERT类似,都是用的了transformer的encoder,但是GPT只是考虑了上文而没有考虑下文,训练的trick也不太一样。
采用的是transformer的decoder模块
word2vec本质上没有解决OOV的问题,fasttext解决了OOV的问题,因为引入了subwords,Glove本质上也是没解决OOV问题,ELMo只是利用上下文来Embedding词向量,解决一词多义的问题。如果要改进的话就可以加入WordPiece或者BPE的方法。
中文解决OOV,可用大规模预训练的bert,基于字的embedding。
- 特征抽取器的比较(CNN/RNN/LSTM/Transformer)
RNN
对于RNN,他主要能考虑到了序列信息,对于当前时刻输入的单词的词向量,他会利用tanh激活函数结合之前的文本序列信息来得到当前时刻的状态或者输出。在encoder-decoder模型中,他会把之前所有的序列信息加入到隐藏层来得到最后的decoder的输入。但是针对较短的文本序列问题,RNN可以有效的抓住之前的开始的文本序列信息。但是如果序列很长,这种线性序列结构在反向传播的时候容易导致严重的梯度消失或梯度爆炸问题
LSTM
LSTM的提出主要是为了解决长依赖问题和针对RNN的梯度消失的问题。LSTM的实现主要就是门机制,主要包括遗忘门,输入门,输出门三个门。在门控机制中的遗忘门中,通过sigmoid函数来决定需要遗忘上一个状态中哪些信息,在输入门中通过sigmoid函数结合当前输入和上一时刻的状态输出来决定更新cell中的信息。从宏观上来看,LSTM中有一个cell单元贯穿始终,使得中间状态信息直接向后传播。因此这使得LSTM可以有效的解决长依赖问题。
但是一个主要的问题是RNN和LSTM等序列模型在计算当前时刻的信息时需要加入之前一个时刻的的隐藏层状态信息。由于整个模型是一个序列依赖关心,这也就限制了整个模型必须是串行计算,无法并行计算。
CNN
对于CNN,他的实现或者说特征提取主要靠卷积层来提取特征,池化层来选择特征,然后使用全连接层来分类。其中卷积层提取特征主要是依靠卷积核对输入做卷积就可以得到特征。那么池化层一般选择使用最大池化来提取最主要的特征,在后面的全连接层中根据提取到的特征进行分类或者其他一些操作。但是卷积层中的CNN因为卷积核的存在,他依旧类似于N-gram,但是Dilated CNN的出现,使得CNN不是连续捕获特征,而是类似于跳一跳方式扩大捕获特征的距离。对位置信息敏感的序列可以抛弃掉max_pooling层。相对于LSTM, CNN的主要优势在于并行的能力。首先对于某个卷积核来说,每个滑动窗口位置之间没有依赖关系,所以可以并行计算;另外,不同的卷积核之间也没什么相互影响,所以也可以并行计算。
Transformer
针对Transformer,首先他打破了CNN和RNN框架。原文中主要是介绍self-attention的作用,但是核multi-attention、FFNN等也有很大的作用。Transformer也是跟CNN一样限定了句子的最大长度,句子长度不足的采用0进行padding。 在self-attention中,他打破了序列信息,而是当前单词可以和句子中任意一个单词编码,集成到embedding里面去,此外transformer利用sin/cos位置函数来进行位置编码。(Bert等模型则给每个单词一个Position embedding,随机初始化的)。针对长距离依赖特征的问题,Self -attention天然就能解决这个问题,因为在集成信息的时候,当前单词和句子中任意单词都发生了联系。所以Transformer也是支持并行计算的。
CNN和LSTM如何选择?
CNN 和 LSTM是深度学习中使用最多,最为经典的两个模型,在NLP中,他们都可以用于提取文本的上下文特征,实际任务中该如何选择呢? 实际中我们主要考虑三个因素:
综合上面三个因素的比较,CNN比较适合上下文依赖较短且对相对位置信息不敏感的场景,如情感分析,情感分析里的一些难点如双重否定,在上下文中的距离都离得不远,LSTM适合需要长距离特征,且依赖相对位置信息的场景。
BERT本身是在Transformer的基础上提出的一个模型。那么BERT就是使用了Transformer的encoder模块来作为基本组成单元以全连接的方式来搭建的BERT模型。BERT的预训练过程是可以分为两个部分来看。第一是mask language model,另一个是next sentence prediction。
在 mask language model中是对当前输入的文本序列中15%的单词进行随机mask,然后用其上下文来做预测。但是该方法有一个问题,因为是mask15%的词,其数量已经很高了,这样就会导致某些词在fine-tuning阶段从未见过,为了解决这个问题,原文是采用了一个训练的技巧,在训练过程中80%的时间是使用mask代替该词,10%的时间是随机取一个词来代替mask,再10%的时间是保持不变。
注意一个点就是为什么要用随机词:主要因为Transformer要保持对每个输入token分布式的表征,否则Transformer很可能会记住这个[mask]就是某一个特定的单词。
在next sentence preddiction中是为每个文本序列添加一个头和尾,来预测第二个句子是否是第一个句子逻辑上的下一句。然后使用头部添加的[cls]来做进一步的处理得到预测结果。
在实际的训练过程中,以上两个模型是联合在一起训练的,BERT的loss函数也是两个子任务loss函数的相加。
BERT的输入是三种类型的词向量(segment embedding,position embedding,word embedding),BERT在训练的过程中,每一层学到的内容并不一样,在较低的层次学到的更多的是句法和语法信息,在较高的层次学到的更多的是语义信息,因此在实际使用的时候也可以根据不同的需求调整BERT的结构。
GPT采用的也是Transformer的encoder模型,但是他是单向的,只考虑了当前单词的上文,而没有考虑下文。
Transformer本身是一个encoder-decoder模型,那么也就可以从encoder和decoder两个方面介绍。
对于encoder,原文是由6个相同的大模块组成,每一个大模块内部又包含多头self-attention模块和前馈神经网络模块。尤其是对于多头self-attention模块相对与传统的attention机制能更关注输入序列中单词与单词之间的依赖关系,让输入序列的表达更加丰富。同时这里的encoder模块也是BERT的一个主要的组成模块。
对于decoder模块,原文中也是包含了6个相同的大模块,每一个大模块由self-attention模块,encoder-decoder交互模块以及前馈神经网络模块三个子模块组成。其self-attention模块和前馈神经网络模块和encoder端是一致的;对于encoder和decoder模块有点类似于传统的attention机制,目的就在于让Decoder端的单词(token)给予Encoder端对应的单词(token)“更多的关注(attention weight)”。
ELMO 本身是个根据当前上下文对 Word Embedding 动态调整的思路
之前的词向量缺点是对于每一个单词都有唯一的一个embedding表示, 而对于多义词显然这种做法不符合直觉, 而单词的意思又和上下文相关, ELMo的做法是我们只预训练language model,而word embedding是通过输入的句子实时输出的, 这样单词的意思就是上下文相关的了,这样就很大程度上缓解了歧义的发生.
在此之前的 Word Embedding 本质上是个静态的方式,所谓静态指的是训练好之后每个单词的表达就固定住了,以后使用的时候,不论新句子上下文单词是什么,这个单词的 Word Embedding 不会跟着上下文场景的变化而改变,所以对于比如 Bank 这个词,它事先学好的 Word Embedding 中混合了几种语义,在应用中来了个新句子,即使从上下文中(比如句子包含 money 等词)明显可以看出它代表的是「银行」的含义,但是对应的 Word Embedding 内容也不会变,它还是混合了多种语义。这是为何说它是静态的,这也是问题所在。
GloVe的全称叫Global Vectors for Word Representation,它是一个基于全局词频统计**(count-based & overall statistics)的**词表征(word representation)工具,它可以把一个单词表达成一个由实数组成的向量,这些向量捕捉到了单词之间一些语义特性,比如相似性(similarity)、类比性(analogy)等。我们通过对向量的运算,比如欧几里得距离或者cos/sin相似度,可以计算出两个单词之间的语义相似性。
- 神经网络详解,正向传播和反向传播
- 深度神经网络(DNN)
DNN也称之为多层感知机(MLP) (我认为就是多个简单的线性(Linear)层的叠加)
- 从此明白了卷积神经网络(CNN)
1. 卷积神经网络vs传统神经网络
其实现在回过头来看,CNN跟我们之前学习的神经网络,也没有很大的差别。传统的神经网络,其实就是多个FC层叠加起来。CNN,无非就是把FC改成了CONV和POOL,就是把传统的由一个个神经元组成的layer,变成了由filters组成的layer。
2. 参数共享机制
我们对比一下传统神经网络的层和由filters构成的CONV层:
假设我们的图像是8×8大小,也就是64个像素,假设我们用一个有9个单元的全连接层:
那这一层我们需要多少个参数呢?需要 64×9 = 576个参数(先不考虑偏置项b)。因为每一个链接都需要一个权重w
那我们看看 同样有9个单元的filter是怎么样的:
其实不用看就知道,有几个单元就几个参数,所以总共就9个参数!因为,对于不同的区域,我们都共享同一个filter,因此就共享这同一组参数。这也是有道理的,通过前面的讲解我们知道,filter是用来检测特征的,那一个特征一般情况下很可能在不止一个地方出现,比如“竖直边界”,就可能在一幅图中多出出现,那么 我们共享同一个filter不仅是合理的,而且是应该这么做的。
由此可见,参数共享机制,让我们的网络的参数数量大大地减少。这样,我们可以用较少的参数,训练出更加好的模型,典型的事半功倍,而且可以有效地 避免过拟合。同样,由于filter的参数共享,即使图片进行了一定的平移操作,我们照样可以识别出特征,这叫做 “平移不变性”。因此,模型就更加稳健了。
3. 连接的稀疏性
由卷积的操作可知,输出图像中的任何一个单元,只跟输入图像的一部分有关系:
而传统神经网络中,由于都是全连接,所以输出的任何一个单元,都要受输入的所有的单元的影响。这样无形中会对图像的识别效果大打折扣。比较,每一个区域都有自己的专属特征,我们不希望它受到其他区域的影响。
正是由于上面这两大优势,使得CNN超越了传统的NN,开启了神经网络的新时代。
4. 经典CNN模型
https://www.jianshu.com/p/4a84885f787a
- 知乎上对RNN及其变体非常好的一个解读
- 针对RNN的解读_英文解读
- 利用MLP来解释RNN
- 视频加动图讲解RNN、LSTM、GRU
- 利用GRU对随机文本做分类
Q1. 一个sentence是如何在词向量化后喂入到RNN的?
首先明确一个RNN的time_step是和句子中token的数量相同的。比如一个RNN定义的是10个time_step,那么这个句子必须值包含10个token,如果不够10个就用0进行padding。
接下来每一个time_step喂入一个词向量化后的token然后得到一个output,或者最后取一个隐藏层状态。
比如这里有一句话,sentence=“我爱我的国”。进行句字的分词后是: 我 /爱 /我的 /国。可以表示为4个n维的词向量,这里n取8表示。那么喂入到RNN的过程就如下图所示。这里有四个时间步,每个时间步分别喂入“我/ 爱/ 我的/ 国” 四个词向量。
Q2. 如何理解RNN中的time_steps?
上面的例子中可以看出time_step就是喂入数据的长度。此外还可以参照这个博客理解。
1.终于理解了RNN里面的time_step
RNN的一个特点是所有的隐层共享参数(U,V,W),整个网络只用这一套参数。
前向传播计算过程:
s t = t a n h ( U x t + W s t − 1 ) o t = s o f t m a x ( V s t ) s_{t} = tanh(Ux_{t}+Ws_{t-1})\\ o_{t} = softmax(Vs_{t}) st=tanh(Uxt+Wst−1)ot=softmax(Vst)
值得注意的是这里的激活函数tanh也可以换成ReLU,并且更换为ReLU可能会缓解梯度消失的问题,但是ReLU的输出比较大,可能进而导致梯度爆炸的问题。
Q1. 梯度消失
由于采用tanh激活函数,在训练的后期,梯度会变得比较小,如果几个趋于0的值相乘的话,乘积就会变得非常小,就会出现梯度消失现象。同样的情况也会出现在sigmoid函数。由于远距离的时刻的梯度贡献接近于0,因此很难学习到远距离的依赖关系。
解决方案:合适的参数初始化可以减少梯度消失的影响;使用ReLU激活函数;LSTM和GRU架构。
Q2. 梯度爆炸
如果后期的导数非常大,就会产生梯度爆炸的问题。
解决方案:既然在BP过程中会产生梯度消失(就是偏导无限接近0,导致长时记忆无法更新),那么最简单粗暴的方法,设定阈值,当梯度小于阈值时,更新的梯度为阈值。
- Understanding LSTM Networks
- LSTM细节分析理解(pytorch版)
- 人人都能看懂的LSTM
- LSTM模型与前向传播算法
- 难以置信!LSTM和GRU的解析从未如此清晰(动图+视频)
- 循环神经网络RNN、LSTM、GRU原理详解(这个文章对反向传播做了简单的解释)
- 为什么LSTM可以解决梯度消失的问题
相比于RNN, LSTM多出出来一个状态 c t c^t ct(cell states)
⊙ \odot ⊙表示矩阵对应元素相乘, ⊕ \oplus ⊕代表矩阵对应元素相加
LSTM的三个阶段(也就是门机制)
其中, z f z^f zf, z i z^i zi , z o z^o zo 是由拼接向量乘以权重矩阵之后,再通过一个 sigmoid 激活函数转换成0到1之间的数值,来作为一种门控状态。而 z则是将结果通过一个 tanh 激活函数将转换成-1到1之间的值(这里使用 tanh 是因为这里是将其做为输入数据,而不是门控信号)。
遗忘门:以一定的概率控制是否遗忘上一层的cell状态.
f t = s i g m o i d ( W f h t − 1 + U f x t + b f ) f^{t} = sigmoid(W_fh^{t-1}+U_{f}x^{t}+b_{f}) ft=sigmoid(Wfht−1+Ufxt+bf)
输入门(也称选择性记忆门):负责处理当前序列位置的输入。输入门由两部分组成,第一部分使用了sigmoid激活函数,输出为 i t i^t it,第二部分使用了 t a n h tanh tanh激活函数,输出为 a t a^t at, 两者的结果后面会相乘再去更新细胞状态。
i t = s i g m o i d ( W i h t − 1 + U i x t + b i ) a t = t a n h ( W a h t − 1 + U a x t + b a ) i^t = sigmoid(W_ih^{t-1}+U_{i}x^t+b_i)\\ a^t = tanh(W_ah^{t-1}+U_{a}x^t+b_a) it=sigmoid(Wiht−1+Uixt+bi)at=tanh(Waht−1+Uaxt+ba)
细胞状态更新:在研究LSTM输出门之前,我们要先看看LSTM之细胞状态。前面的遗忘门和输入门的结果都会作用于细胞状态 C t C^t Ct 。我们来看看从细胞状态 C t − 1 C^{t−1} Ct−1 如何得到 C t C^t Ct。
细胞状态 C t C^t Ct 由两部分组成,第一部分是 C t − 1 C^{t−1} Ct−1和遗忘门输出 f t f^t ft的乘积,第二部分是输入门的 i t i^t it 和 a t a^t at 的乘积,即:
c t = c t − 1 ⊙ f t + i t ⊙ a t c^t=c^{t-1}\odot f^t + i^t \odot a^t ct=ct−1⊙ft+it⊙at
其中 f t f^t ft是遗忘门的输出
输出门(主要是考虑到有多少cell中的信息被加入到当前的输出状态( h t h_t ht)中)隐藏状态 h t h^t ht的更新由两部分组成,第一部分是 o t o^t ot, 它由上一序列的隐藏状态 h t − 1 h^{t-1} ht−1和本序列数据 x t x^t xt,以及激活函数sigmoid得到,第二部分由隐藏状态 c t c^t ct和tanh激活函数组成, 即:
o t = s i g m o i d ( W o h t − 1 + U o x t + b o ) h t = o t ⊙ t a n h ( c t ) o^t = sigmoid(W_{o}h^{t-1}+U_ox^t+b_o)\\ h^t=o^t \odot tanh(c^t) ot=sigmoid(Woht−1+Uoxt+bo)ht=ot⊙tanh(ct)
LSTM的前向传播算法
RNN为什么存在梯度消失的问题
在反向传播的过程中,梯度是连乘的,当梯度的数值小于1的时候,会导致远距离的梯度经过累乘后在当前时刻变得很小,进而发生梯度消失的问题。在反向传播的过程中,反向传播的公式有一个因式是参数W,如果W中有一个特征值特别大,就会产生梯度爆炸的问题。
LSTM是如何解决梯度消失的问题的
LTSM的解决梯度消失问题主要是通过cell单元。在LSTM内部,cell贯穿始终,并且在这条路径上只有乘和加,梯度是比较稳定的。其他路径上的梯度和RNN差不多,但是当其他路径发生梯度消失的时候,高速公路上的梯度没有消失,那么远距离的梯度就没有消失,也就缓解了梯度消失的问题。但是LSTM不能解决梯度爆炸的问题,因为在其他路径上发生了梯度爆炸,总的梯度依旧是爆炸的。
为什么不把RNN中的tanh激活函数换为ReLU?
ReLU可以在一定程度上缓解梯度消失的问题,但是有ReLU会导致非常大的输出,,最后的结果会变成多个W参数连乘,如果W中存在特征值>1,那么经过反向传播的连乘后就会产生梯度爆炸,RNN仍然无法传递较远的距离
RNN中的梯度消失和梯度爆炸
RNN 中的梯度消失/梯度爆炸和普通的 MLP 或者深层 CNN 中梯度消失/梯度爆炸的含义不一样。MLP/CNN 中不同的层有不同的参数,各是各的梯度;而 RNN 中同样的权重在各个时间步共享,最终的梯度 g = 各个时间步的梯度 g_t 的和。RNN 中总的梯度是不会消失的。即便梯度越传越弱,那也只是远距离的梯度消失,由于近距离的梯度不会消失,所有梯度之和便不会消失。RNN 所谓梯度消失的真正含义是,梯度被近距离梯度主导,导致模型难以学到远距离的依赖关系。
“LSTM 能解决梯度消失/梯度爆炸”是对 LSTM 的经典误解。这里我先给出几个粗线条的结论,详细的回答以后有时间了再扩展:
首先需要明确的是,RNN 中的梯度消失/梯度爆炸和普通的 MLP 或者深层 CNN 中梯度消失/梯度爆炸的含义不一样。MLP/CNN 中不同的层有不同的参数,各是各的梯度;而 RNN 中同样的权重在各个时间步共享,最终的梯度 g = 各个时间步的梯度 g_t 的和。
由 1 中所述的原因,RNN 中总的梯度是不会消失的。即便梯度越传越弱,那也只是远距离的梯度消失,由于近距离的梯度不会消失,所有梯度之和便不会消失。RNN 所谓梯度消失的真正含义是,梯度被近距离梯度主导,导致模型难以学到远距离的依赖关系。
LSTM 中梯度的传播有很多条路径, 这条路径上只有逐元素相乘和相加的操作,梯度流最稳定;但是其他路径(例如 )上梯度流与普通 RNN 类似,照样会发生相同的权重矩阵反复连乘。
LSTM 刚提出时没有遗忘门,或者说相当于 ,这时候在 直接相连的短路路径上, 可以无损地传递给 ,从而这条路径上的梯度畅通无阻,不会消失。类似于 ResNet 中的残差连接。
但是在其他路径上,LSTM 的梯度流和普通 RNN 没有太大区别,依然会爆炸或者消失。由于总的远距离梯度 = 各条路径的远距离梯度之和,即便其他远距离路径梯度消失了,只要保证有一条远距离路径(就是上面说的那条高速公路)梯度不消失,总的远距离梯度就不会消失(正常梯度 + 消失梯度 = 正常梯度)。因此 LSTM 通过改善一条路径上的梯度问题拯救了总体的远距离梯度。
同样,因为总的远距离梯度 = 各条路径的远距离梯度之和,高速公路上梯度流比较稳定,但其他路径上梯度有可能爆炸,此时总的远距离梯度 = 正常梯度 + 爆炸梯度 = 爆炸梯度,因此 LSTM 仍然有可能发生梯度爆炸。不过,由于 LSTM 的其他路径非常崎岖,和普通 RNN 相比多经过了很多次激活函数(导数都小于 1),因此 LSTM 发生梯度爆炸的频率要低得多。实践中梯度爆炸一般通过梯度裁剪来解决。
对于现在常用的带遗忘门的 LSTM 来说,6 中的分析依然成立,而 5 分为两种情况:其一是遗忘门接近 1(例如模型初始化时会把 forget bias 设置成较大的正数,让遗忘门饱和),这时候远距离梯度不消失;其二是遗忘门接近 0,但这时模型是故意阻断梯度流的,这不是 bug 而是 feature(例如情感分析任务中有一条样本 “A,但是 B”,模型读到“但是”后选择把遗忘门设置成 0,遗忘掉内容 A,这是合理的)。当然,常常也存在 f 介于 [0, 1] 之间的情况,在这种情况下只能说 LSTM 改善(而非解决)了梯度消失的状况。
最后,别总是抓着梯度不放。梯度只是从反向的、优化的角度来看的,**多从正面的、建模的角度想想 LSTM 有效性的原因。**选择性、信息不变性都是很好的视角
1.遗忘门是如何遗忘的
把t-1时的长期记忆输入 C t − 1 C^{t-1} Ct−1 乘上一个遗忘因子 f t f^{t} ft 。遗忘因子是由短期记忆 h t − 1 h^{t-1} ht−1 以及事件信息 x t x^{t} xt 来计算。
2.为什么激活函数使用sigmoid和tanh
对于sigmoid函数,门是控制开闭的,全开时值为1,全闭值为0。有开有闭时,值在0到1之间。如果选择的激活函数得到的值不在0,1之间时,通常来说是没有意义的。
对于求值时的激活函数tanh,选取时与深层网络中激活函数选取是一样的,没有行与不行,只有好与不好。
所以,总结来说,门的激活函数只能是值域为0到1的,对于求值的激活函数无特殊要求。
3. RNN梯度消失问题,为什么LSTM和GRU可以解决此类问题
RNN为什么存在梯度消失的问题
在反向传播的过程中,梯度是连乘的,当梯度的数值小于1的时候,会导致远距离的梯度经过累乘后在当前时刻变得很小,进而发生梯度消失的问题。在反向传播的过程中,反向传播的公式有一个因式是参数W,如果W中有一个特征值特别大,就会产生梯度爆炸的问题。
LSTM是如何解决梯度消失的问题的
LTSM的解决梯度消失问题主要是通过cell单元。在LSTM内部,cell贯穿始终,并且在这条路径上只有乘和加,梯度是比较稳定的。其他路径上的梯度和RNN差不多,但是当其他路径发生梯度消失的时候,高速公路上的梯度没有消失,那么远距离的梯度就没有消失,也就缓解了梯度消失的问题。但是LSTM不能解决梯度爆炸的问题,因为在其他路径上发生了梯度爆炸,总的梯度依旧是爆炸的。
为什么不把RNN中的tanh激活函数换为ReLU?
ReLU可以在一定程度上缓解梯度消失的问题,但是有ReLU会导致非常大的输出,,最后的结果会变成多个W参数连乘,如果W中存在特征值>1,那么经过反向传播的连乘后就会产生梯度爆炸,RNN仍然无法传递较远的距离
RNN中的梯度消失和梯度爆炸
RNN 中的梯度消失/梯度爆炸和普通的 MLP 或者深层 CNN 中梯度消失/梯度爆炸的含义不一样。MLP/CNN 中不同的层有不同的参数,各是各的梯度;而 RNN 中同样的权重在各个时间步共享,最终的梯度 g = 各个时间步的梯度 g_t 的和。RNN 中总的梯度是不会消失的。即便梯度越传越弱,那也只是远距离的梯度消失,由于近距离的梯度不会消失,所有梯度之和便不会消失。RNN 所谓梯度消失的真正含义是,梯度被近距离梯度主导,导致模型难以学到远距离的依赖关系。
“LSTM 能解决梯度消失/梯度爆炸”是对 LSTM 的经典误解。这里我先给出几个粗线条的结论,详细的回答以后有时间了再扩展:
1、首先需要明确的是,RNN 中的梯度消失/梯度爆炸和普通的 MLP 或者深层 CNN 中梯度消失/梯度爆炸的含义不一样。MLP/CNN 中不同的层有不同的参数,各是各的梯度;而 RNN 中同样的权重在各个时间步共享,最终的梯度 g = 各个时间步的梯度 g_t 的和。
2、由 1 中所述的原因,RNN 中总的梯度是不会消失的。即便梯度越传越弱,那也只是远距离的梯度消失,由于近距离的梯度不会消失,所有梯度之和便不会消失。RNN 所谓梯度消失的真正含义是,梯度被近距离梯度主导,导致模型难以学到远距离的依赖关系。
3、LSTM 中梯度的传播有很多条路径, 这条路径上只有逐元素相乘和相加的操作,梯度流最稳定;但是其他路径(例如 )上梯度流与普通 RNN 类似,照样会发生相同的权重矩阵反复连乘。
4、LSTM 刚提出时没有遗忘门,或者说相当于 ,这时候在 直接相连的短路路径上, 可以无损地传递给 ,从而这条路径上的梯度畅通无阻,不会消失。类似于 ResNet 中的残差连接。
5、但是在其他路径上,LSTM 的梯度流和普通 RNN 没有太大区别,依然会爆炸或者消失。由于总的远距离梯度 = 各条路径的远距离梯度之和,即便其他远距离路径梯度消失了,只要保证有一条远距离路径(就是上面说的那条高速公路)梯度不消失,总的远距离梯度就不会消失(正常梯度 + 消失梯度 = 正常梯度)。因此 LSTM 通过改善一条路径上的梯度问题拯救了总体的远距离梯度。
6、同样,因为总的远距离梯度 = 各条路径的远距离梯度之和,高速公路上梯度流比较稳定,但其他路径上梯度有可能爆炸,此时总的远距离梯度 = 正常梯度 + 爆炸梯度 = 爆炸梯度,因此 LSTM 仍然有可能发生梯度爆炸。不过,由于 LSTM 的其他路径非常崎岖,和普通 RNN 相比多经过了很多次激活函数(导数都小于 1),因此 LSTM 发生梯度爆炸的频率要低得多。实践中梯度爆炸一般通过梯度裁剪来解决。
7、对于现在常用的带遗忘门的 LSTM 来说,6 中的分析依然成立,而 5 分为两种情况:其一是遗忘门接近 1(例如模型初始化时会把 forget bias 设置成较大的正数,让遗忘门饱和),这时候远距离梯度不消失;其二是遗忘门接近 0,但这时模型是故意阻断梯度流的,这不是 bug 而是 feature(例如情感分析任务中有一条样本 “A,但是 B”,模型读到“但是”后选择把遗忘门设置成 0,遗忘掉内容 A,这是合理的)。当然,常常也存在 f 介于 [0, 1] 之间的情况,在这种情况下只能说 LSTM 改善(而非解决)了梯度消失的状况。
8、最后,别总是抓着梯度不放。梯度只是从反向的、优化的角度来看的,**多从正面的、建模的角度想想 LSTM 有效性的原因。**选择性、信息不变性都是很好的视角
官方API:
https://pytorch.org/docs/stable
nn.LSTM参数详解
正向传播
反向传播
- 一文看懂 Attention 机制,你想知道的都在这里了
- seq2seq中的Attention机制
- 深度学习中Attention Mechanism详细介绍:原理、分类及应用
- 动手推导Self-attention-译文
Attention机制的作用
Attention机制其实就是一系列注意力分配系数,也就是一系列权重参数。他的目的就是减少处理高维输入数据的计算负担,结构化的选取输入的子集,从而降低数据的维度。让系统更加容易的找到输入的数据中与当前输出信息相关的有用信息,从而提高输出的质量。帮助类似于decoder这样的模型框架更好的学到多种内容模态之间的相互关系。
attention机制的数学表达式
直接照抄的参考博客2中的内容
介绍下attention:
attention主要是在encoder-decoder模型中出现,原本的encoder-decoder模型的输出会关注输入的全部信息,而attention是希望关注和当前输出相关的重点局部内容。他的本质其实就是一系列的权重。
传统的Attention机制就是soft-attention。与之相对的是hard-attention,两者的不同如下:
Soft Attention是参数化的(Parameterization),因此可导,可以被嵌入到模型中去,直接训练。梯度可以经过Attention Mechanism模块,反向传播到模型其他部分。
Hard Attention是一个随机的过程。Hard Attention不会选择整个encoder的输出做为其输入,Hard Attention会依概率Si来采样输入端的隐状态一部分来进行计算,而不是整个encoder的隐状态。为了实现梯度的反向传播,需要采用蒙特卡洛采样的方法来估计模块的梯度。
由于soft-attention可以用于反向传播,现在用的attention基本都是soft-attention
1.动手推导Self-attention-译文
self-attention和attention的不同:
- https://bbs.dian.org.cn/topic/136/textcnn%E6%96%87%E6%9C%AC%E5%88%86%E7%B1%BB%E8%AF%A6%E8%A7%A3-%E4%BD%BF%E7%94%A8tensorflow%E4%B8%80%E6%AD%A5%E6%AD%A5%E5%B8%A6%E4%BD%A0%E5%AE%9E%E7%8E%B0%E7%AE%80%E5%8D%95textcnn(非常详细的一个博文)
残差网络就是把输入加入到输出再进行下一步的处理。
sigmoid函数
优点:
缺点:
sigmoid函数在变量取绝对值非常大的正值或负值时会出现饱和现象,意味着函数会变得很平,并且对输入的微小改变会变得不敏感。在反向传播时,当梯度接近于0,权重基本不会更新,很容易就会出现梯度消失的情况,从而无法完成深层网络的训练。
sigmoid函数的输出不是0均值的,会导致后层的神经元的输入是非0均值的信号,这会对梯度产生影响。
计算复杂度高,因为sigmoid函数是指数形式。
tanh函数
tanh函数是 0 均值的,因此实际应用中 Tanh 会比 sigmoid 更好。但是仍然存在梯度饱和与exp计算的问题。
ReLU函数
优点:
使用ReLU的SGD算法的收敛速度比 sigmoid 和 tanh 快。
在x>0区域上,不会出现梯度饱和、梯度消失的问题。
计算复杂度低,不需要进行指数运算,只要一个阈值就可以得到激活值。
缺点:
ReLU的输出不是0均值的。
Dead ReLU Problem(神经元坏死现象):ReLU在负数区域被kill的现象叫做dead relu。ReLU在训练的时很“脆弱”。在x<0时,梯度为0。这个神经元及之后的神经元梯度永远为0,不再对任何数据有所响应,导致相应参数永远不会被更新。
产生这种现象的两个原因:参数初始化问题;learning rate太高导致在训练过程中参数更新太大。
解决方法:采用Xavier初始化方法,以及避免将learning rate设置太大或使用adagrad等自动调节learning rate的算法。
- TF-IDF算法原理及应用
词频(TF)表示词条(关键字)在文本中出现的频率。这个数字通常会被归一化(一般是词频除以文章总词数), 以防止它偏向长的文件。
逆向文件频率 (IDF) :某一特定词语的IDF,可以由总文件数目除以包含该词语的文件的数目,再将得到的商取对数得到。如果包含词条t的文档越少, IDF越大,则说明词条具有很好的类别区分能力。
from nltk.text import TextCollection
from nltk.tokenize import word_tokenize
#首先,构建语料库corpus
sents=['this is sentence one','this is sentence two','this is sentence three']
sents=[word_tokenize(sent) for sent in sents] #对每个句子进行分词
print(sents) #输出分词后的结果
corpus=TextCollection(sents) #构建语料库
print(corpus) #输出语料库
#计算语料库中"one"的tf值
tf=corpus.tf('one',corpus) # 1/12
print(tf)
#计算语料库中"one"的idf值
idf=corpus.idf('one') #log(3/1)
print(idf)
#计算语料库中"one"的tf-idf值
tf_idf=corpus.tf_idf('one',corpus)
print(tf_idf)
样本选择上:
样本权重上
预测函数:
并行计算:
偏差和方差
典型的bagging算法:RF
典型的boosting算法:Adaboost, 提升树,GBDT,XGBoost
Bagging 是 Bootstrap Aggregating的简称,意思就是再取样 (Bootstrap) 然后在每个样本上训练出来的模型取平均,所以是降低模型的方差. Bagging 比如 Random Forest 这种先天并行的算法都有这个效果。
Boosting 则是迭代算法,每一次迭代都根据上一次迭代的预测结果对样本进行加权,所以随着迭代不不断进行行,误差会越来越小,所以模型的偏差会不不断降低。这种算法无法并行。
CART回归树的关键师选取(s,j);s是某个特征,j是划分点
CART就是递归的把所有的数据集划分为N个域,然后取每个域中的平均值作为当前叶节点的输出。选择哪个(s,j)对来作为当前结点中数据的的分裂依据是根据按照选定的(s,j)划分后,损失函数是不是最小。
随机森林本质上就是利用数据集训练多颗决策树。在预测的时候利用所有决策树中的众数来作为随机森林最后的输出。
随机森林算法
提升树是boosting算法的一种,本质上可以理解为是一个加法模型,他的基分类器是回归树模型。他的损失函数是均方误差损失函数,从公式的推到中可以看出。提升树的本质就是拟合残差。
提升树算法
f m ( x ) = f m − 1 ( x ) + T ( x ; θ m ) f_m(x) = f_{m-1}(x) + T(x;\theta_{m}) fm(x)=fm−1(x)+T(x;θm)
θ ^ m = a r g m i n ∑ i = 1 N ( y i , f m − 1 ( x i ) + T ( x i ; θ m ) ) \hat{\theta}_{m} = argmin\sum{^N_{i=1}}(y_i, f_{m-1}(x_i)+T(x_i;\theta_{m})) θ^m=argmin∑i=1N(yi,fm−1(xi)+T(xi;θm))
当采用均方误差损失函数的时候,其loss函数变为:
L ( y , f m − 1 ( x ) + T ( x ; θ m ) ) = [ y − f m − 1 ( x ) − T ( x ; θ m ) ] 2 = [ γ − T ( x ; θ m ) ] L(y,f_{m-1}(x)+T(x;\theta_{m}))\\=[y-f_{m-1}(x)-T(x;\theta_{m})]^2\\=[\gamma-T(x;\theta_{m})] L(y,fm−1(x)+T(x;θm))=[y−fm−1(x)−T(x;θm)]2=[γ−T(x;θm)]
其中 f m − 1 ( x ) f_{m-1}(x) fm−1(x)是之前得到的树的预测值; γ \gamma γ 是残差(残差就可以使用回归树来拟合);
Adaboost是一种自适应boosting算法,他的自适应体现在每训练完一个分类器,他会根据训练结果更新当前的数据集的权重。增大被错误分类的数据的权重,减少被正确分类的数据,然后利用更新以后的数据再次训练一个弱分类器并加入到之前得到的强分类器里。如此循环训练直到错误率达到某一个比较的值或者最大的迭代次数。
5.1 整个Adaboost 迭代算法就3步:
5.2 Adaboost算法流程
给定一个训练数据集T={(x1,y1), (x2,y2)…(xN,yN)},yi属于标记集合{-1,+1},Adaboost的目的就是从训练数据中学习一系列弱分类器或基本分类器,然后将这些弱分类器组合成一个强分类器。
步骤1. 首先,初始化训练数据的权值分布。每一个训练样本最开始时都被赋予相同的权值:1/N。
步骤2. 进行多轮迭代,用m = 1,2, …, M表示迭代的第多少轮
a. 使用具有权值分布Dm的训练数据集学习,得到基本分类器(选取让误差率最低的阈值来设计基本分类器):
b. 计算Gm(x)在训练数据集上的分类误差率
由上述式子可知,Gm(x)在训练数据集上的误差率em就是被Gm(x)误分类样本的权值之和。
c. 计算Gm(x)的系数,am表示Gm(x)在最终分类器中的重要程度(目的:得到基本分类器在最终分类器中所占的权重。注:这个公式写成 α m = 1 / 2 l n ( ( 1 − e m ) / e m ) \alpha_m=1/2ln((1-e_m)/e_m) αm=1/2ln((1−em)/em)更准确,因为底数是自然对数e,故用In,写成log容易让人误以为底数是2或别的底数,下同):
由上述式子可知, e m < = 1 / 2 e_m <= 1/2 em<=1/2时, α m > = 0 \alpha_m >= 0 αm>=0,且 α m \alpha_m αm随着 e m e_m em的减小而增大,意味着分类误差率越小的基本分类器在最终分类器中的作用越大。
d. 更新训练数据集的权值分布(目的:得到样本的新的权值分布),用于下一轮迭代
使得被基本分类器Gm(x)误分类样本的权值增大,而被正确分类样本的权值减小。就这样,通过这样的方式,AdaBoost方法能“重点关注”或“聚焦于”那些较难分的样本上。
其中,Zm是规范化因子,使得Dm+1成为一个概率分布:
步骤3. 组合各个弱分类器
从而得到最终分类器,如下:
GBDT应该和提升树联系在一起。提升树的损失函数是均方误差损失函数,此时就相当于利用一颗回归树来拟合当前数据的残差,但是当损失函数不再是均方误差的时候,提升树就显得无能为力了。因此为了扩展到其他损失函数上,提出了GBDT算法。当GBDT算法采用均方损失的时候,GBDT和提升树就很接近了。GBDT是借鉴于梯度下降法,其基本原理是根据当前模型损失函数的负梯度信息来训练新加入的弱分类器,然后将训练好的弱分类器以累加的形式结合到现有模型中,他的基分类器是采用回归树。
GBDT拓展到用弱分类器来拟合负梯度主要是利用了一阶泰勒展开式来看,从泰勒展开式的第二项看,如果要使得损失函数下降,只需要使得要训练的基分类器的输出在数值上等于损失函数在当前点的负值就可以。也就是利用基分类器去拟合负梯度。
6.1 GBDT拟合的为什么是负梯度
优化目标函数: ∑ i = 1 N L ( y i , h m − 1 ( x i ) + f m ( x i ) ) \sum{_{i=1}^N}L(y_{i},h_{m-1}(x_{i})+f_{m}(x_{i})) ∑i=1NL(yi,hm−1(xi)+fm(xi))
最小化上述目标函数,也就是每添加一个弱分类器就使得损失函数下降一部分。利用泰勒公式对上述问题进行近似来回答为什么GBDT拟合的是负梯度
L ( y i , h m − 1 ( x i ) + f m ( x i ) ) = L ( y i , h m − 1 ( x i ) ) + ∂ L ( y i , f m − 1 ( x i ) ) ∂ ( f m − 1 ( x ) ) ∗ f m ( x i ) L(y_{i},h_{m-1}(x_{i})+f_{m}(x_{i})) = \\L(y_{i},h_{m-1}(x_{i}))+ \frac{\partial{L(y_{i},f_{m-1}(x_{i}))}}{\partial(f_{m-1}(x))}*f_{m}(x_{i}) L(yi,hm−1(xi)+fm(xi))=L(yi,hm−1(xi))+∂(fm−1(x))∂L(yi,fm−1(xi))∗fm(xi)
当
f m ( x i ) = − ∂ L ( y i , f m − 1 ( x i ) ) ∂ ( f m − 1 ( x ) ) f_{m}(x_{i}) = -\frac{\partial{L(y_{i},f_{m-1}(x_{i}))}}{\partial(f_{m-1}(x))} fm(xi)=−∂(fm−1(x))∂L(yi,fm−1(xi))则肯定有 L ( y i , h m − 1 ( x i ) + f m ( x i ) ) < L ( y i , h m − 1 ( x i ) ) L(y_{i},h_{m-1}(x_{i})+f_{m}(x_{i}))
也就是利用新的弱分类器取拟合当前损失函数的负梯度就会使得整个损失函数不断减小。当损失函数是平方损失的时候,负梯度就是残差,也就是说拟合残差是GBDT中的一种特殊情况。
6.2 在GBDT种如何确定当前的点是否要分裂的依据
我们可以把损失函数由每个样本的表达形式转化为每个叶节点表达的形式,由此可以计算出该叶节点分裂前后的损失函数减少值,如果该值的减少符合预期就对当前结点进行分裂,否则就不分裂。
6.3 QA
Q1. GBDT为什么是拟合负梯度
GBDT本身就是一个回归算法,回归算法的本质就是最小化损失函数,而最小化损失函数的本质又是梯度下降。这里采用平方差和作为损失函数,其求导正好是残差,所以就相当于是利用提升树来集合残差。
Q2. 在GBDT中为什么使用泰勒公式推导梯度下降算法
泰勒公式推导只是一种方法
Q3. GBDT和提升树区别
提升树模型每一次的提升都是靠上次的预测结果与训练数据的label值差值作为新的训练数据进行重新训练,由于原始的回归树指定了平方损失函数所以可以直接计算残差,而梯度提升树针对一般损失函数,所以采用负梯度来近似求解残差,将残差计算替换成了损失函数的梯度方向,将上一次的预测结果带入梯度中求出本轮的训练数据。这两种模型就是在生成新的训练数据时采用了不同的方法。
Q4. GBDT是如何防止过拟合的
Q5. GBDT是如何实现正则化的
Q6. GBDT的优缺点
GBDT主要的优点有:
1) 可以灵活处理各种类型的数据,包括连续值和离散值。
2) 在相对少的调参时间情况下,预测的准备率也可以比较高。这个是相对SVM来说的。
3)使用一些健壮的损失函数,对异常值的鲁棒性非常强。比如 Huber损失函数和Quantile损失函数。
GBDT的主要缺点有:
1)由于弱学习器之间存在依赖关系,难以并行训练数据。不过可以通过自采样的SGBT来达到部分并行。
Python机器学习笔记:XgBoost算法
XGBoost和GBDT的不同
XGBoost的详细公式推导
目前看到的对XGBoost最好的解读
20道XGBoost面试题
XGboost和GBDT是boosting算法的一种,XGBoost其本质上还是一个GBDT的工程实现,但是力争把速度和效率发挥到极致。
GBDT算法本身也是一种加法模型,是对提升树一种优化。他使得boosting算法可以拓展到应对任何损失函数类别。理论中,针对GBDT的损失函数做了一个一阶泰勒近似,一阶泰勒近似的结果就是一个一阶导数,也就是梯度。因此本质上GBDT是对损失函数的负梯度的一个拟合,当损失函数采用均方误差损失的时候,GBDT拟合的负梯度就是残差。在这个过程中,GBDT使用的基分类器是CART回归树。
对于XGBoost,是GBDT的一种优化。但是相对GBDT, XGBoot主要在以下几个方面做了优化:
为什么要使用二阶导数信息:二阶信息本身就能让梯度收敛更快更准确
目标函数
其中正则项控制着模型的复杂度,包括了叶子节点数目T和leaf score的L2模的平方:
XGBoost根据增益情况计算出来选择哪个特征作为分割点,而某个特征的重要性就是它在所有树中出现的次数之和。
决策树的学习最耗时的一个步骤就是对特征值进行排序,在进行节点分裂时需要计算每个特征的增益,最终选增益大的特征做分裂,各个特征的增益计算可开启多线程进行。而且可以采用并行化的近似直方图算法进行节点分裂。
只想到三点,特征排序,特征切分和直方图和全排序
1)更快的训练速度和更高的效率:LightGBM使用基于直方图的算法。
2)直方图做差加速:一个子节点的直方图可以通过父节点的直方图减去兄弟节点的直方图得到,从而加速计算。
3)更低的内存占用:使用离散的箱子(bins)保存并替换连续值导致更少的内存占用。
4)更高的准确率(相比于其他任何提升算法):它通过leaf-wise分裂方法(在当前所有叶子节点中选择分裂收益最大的节点进行分裂,如此递归进行,很明显leaf-wise这种做法容易过拟合,因为容易陷入比较高的深度中,因此需要对最大深度做限制,从而避免过拟合。)产生比level-wise分裂方法(对每一层所有节点做无差别分裂,可能有些节点的增益非常小,对结果影响不大,但是xgboost也进行了分裂,带来了务必要的开销)更复杂的树,这就是实现更高准确率的主要因素。然而,它有时候或导致过拟合,但是我们可以通过设置|max-depth|参数来防止过拟合的发生。
5)大数据处理能力:相比于XGBoost,由于它在训练时间上的缩减,它同样能够具有处理大数据的能力。
6)支持并行学习。
7)局部采样:对梯度大的样本(误差大)保留,对梯度小的样本进行采样,从而使得样本数量降低,提高运算速度。
XGBoosting采用预排序,在迭代之前,对结点的特征做预排序,遍历选择最优分割点,数据量大时,贪心法耗时,LightGBM方法采用histogram算法,占用的内存低,数据分割的复杂度更低。
XGBoosting采用level-wise生成决策树,同时分裂同一层的叶子,从而进行多线程优化,不容易过拟合,但很多叶子节点的分裂增益较低,没必要进行跟进一步的分裂,这就带来了不必要的开销;LightGBM采用深度优化,leaf-wise生长策略,每次从当前叶子中选择增益最大的结点进行分裂,循环迭代,但会生长出更深的决策树,产生过拟合,因此引入了一个阈值进行限制,防止过拟合。
这两种模型就是在生成新的训练数据时采用了不同的方法。**对于梯度提升树,其学习流程与提升树类似只是不再使用残差作为新的训练数据而是使用损失函数的梯度作为新的新的训练数据的y值。**但是如果GBDT采用平方损失作为损失函数,其梯度就又是残差。
XGBoost是GBDT的一种工程实现方式,在GBDT的理论推导中,是利用一阶泰勒近似得到了GBDT本质上就是拟合损失函数的负梯度,但是XGBoot是利用到了一阶和二阶信息。二阶信息保证了模型训练的更准确收敛的更快。
GBDT中只是利用回归树来作为他的基分类器,但是XGBoost中还添加了线性分类器。并且在XGBoost的目标函数中添加了正则项来约束最后学习到的模型。
XGBoost在训练的过程中支持列抽样,类似于随机森林可以选择部分特征。这样不仅可以减少过拟合的风险还可以减少计算量。
XGBoot是支持并行的,这也是最主要优于GBDT的一点。XGBoost的并行并不是体现在tree的粒度上,而是体现在特征的粒度上。决策树学习最耗时的一个步骤就是对特征的排序,因为要确定最佳的分割点。但是XGBoost在训练之前预先对数据进行排序,然后保存为block结构,后面的迭代中重复使用这个结构,这就大大减少了计算量。在进行节点的分裂时,要计算每个特征的增益,最后选择大的增益去做分类,那么这里就可以开多线程来进行特征的增益计算。
对于缺失样本,XGBoot可以自动学习出他的裂变方向。缺失值数据会被分到左子树和右子树分别计算损失,选择较优的那一个。如果训练中没有数据缺失,预测时出现了数据缺失,那么默认被分类到右子树。
可并行的近似直方图算法。树节点在进行分裂时,我们需要计算每个特征的每个分割点对应的增益,即用贪心法枚举所有可能的分割点。当数据无法一次载入内存或者在分布式情况下,贪心算法效率就会变得很低,所以xgboost还提出了一种可并行的近似直方图算法,用于高效地生成候选的分割点。
Adaboost是boosting算法,随机森林是bagging算法
Adaboost是每次调整训练集中样本点的权重来训练下一个分类器,但是随机森林是又放回的抽取N个数据集来组成新的训练集来训练弱分类器
Adaboost中每个弱分类器是有权重的,但是随机森林的各个分类器的权重是一样的,最后采取投票的方式得到最后的额结果
Adaboost的各个弱分类器是串行训练的,但是随机森林可以并行训练
随机森林是支持列抽样的。
1)随机森林采用的bagging思想,而GBDT采用的boosting思想。
2)组成随机森林的树可以是分类树,也可以是回归树;而GBDT只由回归树组成。
3)组成随机森林的树可以并行生成;而GBDT只能是串行生成。
4)对于最终的输出结果,随机森林采用多数投票等;而GBDT则是将所有结果累加起来,或者加权累加起来。
5)随机森林对异常值不敏感;GBDT对异常值非常敏感。
6)随机森林是通过减少模型方差提高性能;GBDT是通过减少模型偏差提高性能。
1. bagging和boosting的区别
2. XGBoost和GBDT的区别
3. GBDT的原理和常用调参参数
先用一个初始值去学习一棵树,然后在叶子处得到预测值以及预测后的残差,之后的树则基于之前树的残差不断的拟合得到,从而训练出一系列的树作为模型。
n_estimators基学习器的最大迭代次数,learning_rate学习率,max_lead_nodes最大叶子节点数,max_depth树的最大深度,min_samples_leaf叶子节点上最少样本数。
GBDT中的小trick:
GBDT中采用shrinkage来设置步长,这样可以有效避免过拟合。
GBDT适用场景
GBDT几乎可用于所有回归问题(线性/非线性),GBDT的适用面非常广。亦可用于二分类问题(设定阈值,大于阈值为正例,反之为负例)。
**10.1 什么是SVM?**SVM和LR有什么区别
**支持向量机为一个二分类模型,它的基本模型定义为特征空间上的间隔最大的线性分类器。而它的学习策略为最大化分类间隔, 最终可转化为凸二次规划问题求解。**SVM中引入核函数的本质就是针对线性不可分的数据集,寻求一种可以在低纬度进行特征计算然后映射到高纬度进行分类的方法。但是其本质还是针对线性不可分的数据集,映射到高纬度后寻求一个空间几何平面对其进行分割。
SVM和LR的区别
LR为什么不使用核函数:因为在SVM中核函数的本质是只计算支持向量,但是LR中会考虑所有的点,所有点都要两两计算,计算量太过庞大。
10.2 SVM中什么时候用线性核什么时候用高斯核?
10.3 SVM中的软间隔和硬间隔
软间隔和硬间隔的差别就是有没有引入松弛变量
10.4 SVM中为什么要引入对偶问题
由于SVM的变量个数为支持向量的个数,相较于特征位数较少,因此转为对偶问题。通过拉格朗日算子法使带约束的优化目标转为不带约束的优化函数,使得W和b的偏导数等于零,带入原来的式子,再通过转成对偶问题。
10.5 核函数的种类和适用情形(这个回答是有错误的)
线性核、多项式核、高斯核
线性核:样本数量多且维度高;样本数量很多
高斯核:样本数量较少且维数不是很高,对时间不敏感
为什么高斯核可以映射无穷维度:因为把泰勒公式带入高斯核函数将得到一个无穷维度的映射
10.6 SVM的损失函数
合页损失函数
为什么一般利用L2解决过拟合问题而非L1
因为L1是一个绝对值求和的过程,在反向传播的过程中会涉及到求导,置零,解方程。但是如果给出一个绝对值方程,上述三者就会失效,求最小值就会有很大的麻烦
同样的对于 L1 和 L2 损失函数的选择,也会碰到同样的问题,所以最后大家一般用 L2 损失函数而不用 L1 损失函数的原因就是:因为计算方便!可以直接求导获得取最小值时各个参数的取值。此外还有一点,用 L2 一定只有一条最好的预测线,L1 则因为其性质可能存在多个最优解。当然 L1 损失函数主要就是鲁棒性 (Robust) 更强,对异常值更不敏感。
L1和L2的差别,为什么一个让绝对值最小,一个让平方最小,会有那么大的差别呢?看导数一个是1一个是w便知, 在靠进零附近, L1以匀速下降到零, 而L2则完全停下来了. 这说明L1是将不重要的特征(或者说, 重要性不在一个数量级上)尽快剔除, L2则是把特征贡献尽量压缩最小但不至于为零.
L2平方项是个圆圈,防止过拟合找到最优化,L1是个正方形歪放在坐标轴,选出少量特征。
PCA是比较常见的线性降维方法,通过线性投影将高维数据映射到低维数据中,所期望的是在投影的维度上,新特征自身的方差尽量大,方差越大特征越有效,尽量使产生的新特征间的相关性越小。
PCA算法的具体操作为对所有的样本进行中心化操作,计算样本的协方差矩阵,然后对协方差矩阵做特征值分解,取最大的n个特征值对应的特征向量构造投影矩阵。
9. 梯度消失梯度爆炸原因与解决方式
- 梯度消失和梯度爆炸
概念和表现:在反向求导的过程中,前面每层的梯度都是来自后面每层梯度的乘积,当层数过多时,有可能产生梯度不稳定,也就是梯度消失或者梯度爆炸,他门的本质都是因为梯度反向传播中的连乘效应。他的表现就是随着网络层数的加深,但是模型的效果却降低了。
梯度消失产生的原因: 隐藏层数量太大,使用了不合适的激活函数
**梯度爆炸产生的原因:**隐藏层数量太大,权重的初始化值过大,使用了不合适的激活函数
如何解决:
预训练加微调
加入正则化
梯度修剪
选择合适的激活函数,relu、leakrelu、elu等激活函数
batchnorm
Batchnorm本质上是解决反向传播过程中的梯度问题。batchnorm全名是batch normalization,简称BN,即批规范化,通过规范化操作把数据拉回到激活函数的梯度敏感区域,使得模型有一个更易于收敛。
LSTM
LSTM全称是长短期记忆网络(long-short term memory networks),是不那么容易发生梯度消失的,主要原因在于LSTM内部复杂的“门”(gates),如下图,LSTM通过它内部的“门”可以接下来更新的时候“记住”前几次训练的”残留记忆“,因此,经常用于生成文本中。
减少网络隐藏层的数量
选择合适的初始化手段
为什么ReLU可以避免梯度消失的问题
ReLU的正半轴是线性的,他的导数是1且是一个固定值,所以容易避免发生梯度消失和梯度爆炸的问题。但是他并不能从根本上解决梯度消失的问题,因为当输入是小于0的时候,就会把Relu的负半轴激活,在这一侧,ReLU的输出就是0,导数也是0, 他依旧无法避免梯度消失的问题。
为什么选用ReLu而不是sigmoid,因为sigmoid只在0的附近具有比较好的特性,随着数据的增大结合减小,梯度就会趋近于0,进而产生梯度消失的现象。但是Relu在大于0的区间导数是一个常数,不存在梯度消失或者梯度爆炸的问题,并且他使得模型的训练速度更快,更容易收敛。
在自然语言处理领域,被验证为有效的数据增强算法相对要少很多,下面我们介绍几种常见方法。
神经网络在训练的时候随着网络层数的加深,激活函数的输入值的整体分布逐渐往激活函数的取值区间上下限靠近,从而导致在反向传播时低层的神经网络的梯度消失。而BatchNormalization的作用是通过规范化的手段,将越来越偏的分布拉回到标准化的正态分布,使得激活函数的输入值落在激活函数对输入比较敏感的区域,从而使梯度变大,加快学习收敛速度,避免梯度消失的问题。
- layer normalization应该放在激活函数的前面还是后面?
Pre-LN相较传统Transformer的Post-LN在训练阶段可以不需要warm-up并且模型更加稳定、收敛更快。(warm-up可以避免全连接层的不稳定的剧烈改变。在有了warm-up之后,模型能够学得更稳定)
- BN和LN的区别
- Batch Normalization 的处理对象是对一批样本, Layer Normalization 的处理对象是单个样本。
- Batch Normalization 是对这批样本的同一维度特征做归一化, Layer Normalization 是对这单个样本的所有维度特征做归一化。
2个3*3的卷积核串联和5*5的卷积核有相同的感知野,前者拥有更少的参数。多个3*3的卷积核比一个较大尺寸的卷积核有更多层的非线性函数,增加了非线性表达,使判决函数更具有判决性。
在神经网络的训练过程中,对于神经单元按一定的概率将其随机从网络中丢弃,从而达到对于每个mini-batch都是在训练不同网络的效果,防止过拟合。
防止过拟合方法的一种,与dropout不同的是,它不是按概率将隐藏层的节点输出清0,而是对每个节点与之相连的输入权值以一定的概率清0。
通过神经网络解决多分类问题时,最常用的一种方式就是在最后一层设置n个输出节点,无论在浅层神经网络还是在CNN中都是如此,比如,在AlexNet中最后的输出层有1000个节点,而即便是ResNet取消了全连接层,也会在最后有一个1000个节点的输出层。
一般情况下,最后一个输出层的节点个数与分类任务的目标数相等。假设最后的节点数为N,那么对于每一个样例,神经网络可以得到一个N维的数组作为输出结果,数组中每一个维度会对应一个类别。在最理想的情况下,如果一个样本属于k,那么这个类别所对应的的输出节点的输出值应该为1,而其他节点的输出都为0,即[0,0,1,0,….0,0],这个数组也就是样本的Label,是神经网络最期望的输出结果,交叉熵就是用来判定实际的输出与期望的输出的接近程度。
- 优化算法Optimizer比较和总结
https://www.zhihu.com/question/27239198
采用文本增强的方法进行解决
使用坐标轴下降法进行优化
坐标下降法属于一种非梯度优化的方法,它在每步迭代中沿一个坐标的方向进行线性搜索(线性搜索是不需要求导数的),通过循环使用不同的坐标方法来达到目标函数的局部极小值。
我们得到混淆矩阵后,可以计算出 TPR 和 FPR ,然后用 FPR 做横轴,TPR 做纵轴,画出一条 FPR-TPR 曲线,就是 ROC 曲线,ROC 曲线下方的面积就是 AUC。我们计算 AUC 的时候可以根据定义取多个 threshold,用矩形的面积来拟合曲线下面积。但是在实际使用中,这种算法效率很低,因为对于每一个 threshold 都需要计算 TP、TN、FP、FN,实际过程中人们是使用 rank 来做。
最大似然估计提供了一种给定观察数据来评估模型参数的方法,而最大似然估计中的采样满足所有采样都是独立分布的假设。
最大后验概率是根据经验数据获难以观察量的点估计,与最大似然估计最大的不同是最大后验概率融入了要估计量的先验分布在其中,所以最大后验概率可以看做规则化的最大似然估计。
概率是指在给定参数 θ \theta θ 的情况下,样本的随机向量X=x的可能性。而似然表示的是在给定样本X=x的情况下,参数 θ \theta θ 为真实值的可能性。一般情况,对随机变量的取值用概率表示。而在非贝叶斯统计的情况下,参数为一个实数而不是随机变量,一般用似然来表示。
生成器
python生成器是一个返回可以迭代对象的函数,可以被用作控制循环的迭代行为。生成器类似于返回值为数组的一个函数,这个函数可以接受参数,可以被调用,一般的函数会返回包括所有数值的数组,生成器一次只能返回一个值,这样消耗的内存将会大大减小。
is用来判断连个变量引用的对象是否为同一个,==用于判断应用对象的值是否相等。
dict查找速度快,占用的内存较大,list查找速度慢,占用内存较小,dict不能用来存储有序集合。Dict用{}表示,list用[]表示。
dict是通过hash表实现的,dict为一个数组,数组的索引键是通过hash函数处理后得到的,hash函数的目的是使键值均匀的分布在数组中。
装饰器的作用就是为已经存在的函数或对象添加额外的功能。
Python代码的执行由Python虚拟机(解释器)来控制。Python在设计之初就考虑要在主循环中,同时只有一个线程在执行,就像单CPU的系统中运行多个进程那样,内存中可以存放多个程序,但任意时刻,只有一个程序在CPU中运行。同样地,虽然Python解释器可以运行多个线程,只有一个线程在解释器中运行。
python中的垃圾回收机制是使用计数器实现的。
return
是函数返回值,当执行到return
,后续的逻辑代码不在执行
yield
是创建迭代器,可以用for
来遍历,有点事件触发的意思
#encoding:UTF-8
def yield_test(n):
for i in range(n):
yield call(i) # 它会立即把call(i)输出,成果拿出来后才会进行下一步,所以 i, ',' 会先执行
print("i=",i) # 后执行 #做一些其它的事情
print("do something.") # 待执行,最后才执行一遍
print("end.")
def call(i):
return i*2
#使用for循环
for i in yield_test(5):
print(i,",") # 这里的 i 是 call(i)
import sys
from string import Template
a=10;
b=100
name="NEG"
t=Template("Hello $name!")
res=t.substitute(name=name)
print (res) # 模板
print("%d" %a) # %形式
print("{}".format(b)) # format形式
print(f"{a}") # f形式
1.rnn真的就梯度消失了吗?
RNN处理短序列文本的时候梯度并没有消失,但是处理长序列文本的时候,经过累乘结果会趋近于0,进而发生梯度消失。
2.lstm到底解决了什么?解决了梯度消失?
LSTM主要是通过门机制来缓解梯队消失的问题,尤其引入了遗忘门和输入门来对上一个状态进行选择性遗忘和对cell进行选择性更新,从宏观来看,他引入的cell也是贯穿整个网络的始终,前期的信息可以更好的保留到最后,很好的缓解了长依赖的问题。
3.gru结构和网络轻量化(减少参数)
1.LSTM三种门以及sigmoid函数对每个门的作用
LSTM的三个门主要包括遗忘门、输入门、输出门
sigmoid就是一个门控机制,可以说就是一个门。当sigmoid的输出为1时表示门全部打开,为0时表示全部关闭。在遗忘门中控制有多少 h t − 1 h_{t-1} ht−1的信息被加入到cell,在输入门中表示有多少 x t x_t xt中有多少信息被用来更新cell,在输出门中表示有多少cell中的信息被用来考虑组成当前结点的隐藏层状态输出。
2.Self-attention的Query,Key,Value分别是什么。乘积是什么和什么的Query和Key相乘
为什么要使用Q K V? self-attention使用Q、K、V,这样三个参数矩阵独立,模型的表达能力和灵活性显然会比只用Q、V或者只用V要好些
Q K V是通过词嵌入乘以训练过程中创建的3个训练矩阵而产生的向量。这里面的训练矩阵是随机初始化的。
Z = s o f t m a x ( Q ⋅ K T d k ) ⋅ V Z = softmax(\frac{Q\cdot K^T}{\sqrt{d_k}})\cdot V Z=softmax(dkQ⋅KT)⋅V
为什么要对self attention的q k v做一个线性变换?如果不对qkv进行变换的话,那么qkv三个矩阵应该是相同的,那么每个单词自己的q和k相乘的结果一定是最匹配最大的,这显然不是很科学
3. Self-attention的乘法计算和加法计算有什么区别?什么时候乘比较好,什么时候加
区别:
论文中指出当 d k d_{k} dk比较小的时候,乘法注意力和加法注意力效果差不多;但当 d k d_{k} dk比较大的时候,如果不使用比例因子,则加法注意力要好一些,因为乘法结果会比较大,容易进入softmax函数的“饱和区”,梯度较小。
4. 为什么要除以一个根号?也是归一化
除以 d k \sqrt d_{k} dk是为了得到更平稳的梯度。因为随着 d k d_k dk的增大, q ⋅ k q \cdot k q⋅k点积后的结果也随之增大,这样会将softmax函数推入梯度非常小的区域,使得收敛困难。因此对其做一个缩放,是为了得到更平稳的梯度,也有利于模型的收敛。
5. 多头注意力机制的原理是什么?
multi-head就是初始化多个 W Q , W K , W V W^Q, W^K, W^V WQ,WK,WV训练矩阵,针对同一个对象得到多个Q K V 表示向量,进而得到当前对象的多个表示方法,有利于提升得到的词向量表达信息的多样性和丰富性。同时也是让模型去关注不同方面的信息,最后再将各个方面的信息综合起来。
6. Transformer用的是哪种attention机制?
self-attention
7. bert的位置编码和transformer有什么不同
BERT是随机初始化的位置向量,Transformer是利用sin/cos位置函数得到的位置向量。由于BERT的参数量比较大并且训练的语料库也比较大,这两种位置编码在BERT的最后的训练过程中得到的效果都差不多,但是使用随机初始化更方便,并且节省计算时间。
8. bert为什么需要多头, 为什么bert有12层encoder, 如果是QA问题,你知道该如何调整encoder的层数吗?
BERT本身是使用的Transformer的encoder,也就使用了多头注意力机制。这是为了防止以当前处理对象为主导,使得self-attention得到的信息不完整,不丰富。使用多头的目的就是充分考虑序列当中处理对象与其他单词尽可能多的信息,使得得到的向量表达的信息更完善更丰富。
BERT的每个层得到信息是不一样的,越在下面的层学的更多的是语法和句法信息,越往上学得的信息越抽象越高级比如语义特征等。针对QA问题,我们应该增加层数,使用更高级得语义信息。
9. 去掉self-attention是否可以得到词向量
可以得到,因为还存在全连接层,所以还是可以拿到词向量。
10. 为什么要去掉停用词
停用词一般指使用非常广泛或者频率非常高的词。这类词一般会占用存储空间,并且对有效的信息造成干扰,也就是说本身是一个噪声,可能会造成其他重要信息得丢失。因此要对这类词去除。
11. word2vec为什么没有预训练,word2vec和bert的区别,和ELMO的区别
1.bert怎么分词?
- BERT是如何分词的
2.为什么lstm门用tanh?
使用tanh只是一个实验的过程,并且lstm默认的也是tanh,但是当网络层加深后,LSTM依旧面临梯度消失的风险,这个时候还是要使用RELU激活函数来尽可能的避免梯度消失问题。
3. tensorflow与pytorch区别
4. 岭回归和lasso回归的区别
Lasso是加 L1 penalty,也就是绝对值;岭回归是加 L2 penalty,也就是二范数。
从贝叶斯角度看,L1 正则项等价于参数 w 的先验概率分布满足拉普拉斯分布,而 L2 正则项等价于参数 w 的先验概率分布满足高斯分布。
从优化求解来看,岭回归可以使用梯度为零求出闭式解,而 Lasso 由于存在绝对值,在 0 处不可导,只能使用 Proximal Mapping 迭代求最优解。
从结果上看,L1 正则项会使得权重比较稀疏,即存在许多 0 值;L2 正则项会使权重比较小,即存在很多接近 0 的值。
5. L1和L2正则化如何选择
L1正则化可以用来做特征选择,如果只是解决过拟合的问题, L1和L2都可以
6. 如果把激活函数全都换成线性函数,会出现什么问题
如果把激活函数全都换成线性函数会失去非线性性,退化为一个线性回归。如果是分类问题最后有一个 sigmoid 层,则退化为逻辑回归。深度学习能起作用的本质原因就是使用了非线性的激活函数,从而通过很多神经元可以拟合任意一个函数。
7. AUC的含义是什么
假设从所有正样本中随机选取一个样本,把该样本预测为正样本的概率为 p1,从所有负样本中随机选取一个样本,把该样本预测为正样本的概率为p0,p1 > p0 的概率就是 AUC。
1. attention的实现
attention机制主要是用seq2seq模型中,再来encode中我们得到每一个输入对应的隐藏层状态,然后再decoder中比如我们当前的状态是 s t − 1 s_{t-1} st−1, 那么我们可以使用endoder中的 h t h_t ht和 s t − 1 s_{t-1} st−1相乘再利用softmax对其进行处理,这就得到了我们说的attention中的权重,利用上述得到的权重去乘我们的 h t h_t ht,就为我们的输入都分配了一个权重来预测我们后续的 s t s_t st。
1. CNN特性
CNN主要的特性包括:
2. LSTM特性
LSTM主要通过一个cell单元解决的文本在时序上的一个长期依赖关系。LSTM的内部结构主要包括输入门,遗忘门门和输出门三个门结构。主要在遗忘门中,LSTM通过一个sigmoid函数来决定之前得到的隐藏层状态 h t − 1 h_{t-1} ht−1中有多少信息被遗忘。在后续中通过输入门决定cell中有什么信息需要保留或者说更新。
3. embedding模型
早期的包括one hot和词袋模型
固定的词向量模型包括:word2vec和glove
预训练模型包括ELMo、BERT、GPT等
4. RNN有哪些缺点? LSTM为什么比RNN好
- RNN和LSTM的比较
RNN主要的缺点是存在梯度消失,无法解决长依赖问题。但是LSTM的门机制会对前期的信息是否需要保留做出一个判断,并且也会在输入门中来决定哪些信息需要被更新到cell当中。由于cell的存在,这就使得前期的有用信息会得到保留并延续到时间序列的最后,也就解决了长依赖问题。
LSTM的结构更加复杂,这也使得其不容易产生梯度消失。从宏观上来看,LSTM的各个单元之间有一个cell单元贯穿始终,这也就使得LSTM能解决长期依赖的问题。
5. BERT和ELMo比较
6. BERT介绍/Transformer介绍
6.1 Transformer介绍
Transformer本身是一个encoder-decoder模型,那么也就可以从encoder和decoder两个方面介绍。
**对于encoder,**原文是由6个相同的大模块组成,每一个大模块内部又包含多头self-attention模块和前馈神经网络模块。尤其是对于多头self-attention模块相对与传统的attention机制能更关注输入序列中单词与单词之间的依赖关系,让输入序列的表达更加丰富。同时这里的encoder模块也是BERT的一个主要的组成模块。
对于decoder模块,原文中也是包含了6个相同的大模块,每一个大模块由self-attention模块,encoder-decoder交互模块以及前馈神经网络模块三个子模块组成。其self-attention模块和前馈神经网络模块和encoder端是一致的;对于encoder和decoder模块有点类似于传统的attention机制,目的就在于让Decoder端的单词(token)给予Encoder端对应的单词(token)“更多的关注(attention weight)”。
6.2 BERT介绍
BERT其实是一个预训练模型。不同于ELMo中的双向LSTM,他的模型主要组成的内容就是transformer的encoder。此外GPT模型的主要组成模块也是Transformer的encoder,但是GPT只关注当前处理对象的上文,而BERT对transformer的使用不同于GPT而是类似于双向LSTM,利用当前处理对象的上下文信息处理当前的对象。
原文中提到的BERT主要有两种模型。一个是BERT base和BERT large。两者的主要区别是模型的深度和参数量的不同。对于BERT的预训练,原文是提出了两种训练方式,一种是mask language model,这种主要是低输入的token利用mask进行遮掩部分的token,训练BERT来预测被mask的单词。另一种训练方式是next sentence predction,这里主要是输入一个句子对,让BERT来预测这两个句子是否是真正的句子对。但是在实际的训练过程中,两者是一块进行的。
针对BERT的 整个训练模型,BERT的低层次模型更倾向于语法特征的学习,高层偏向于语义特征的学习。
目前还出现了bert_as_sevice来直接使用BERT。以上就是对BERT的一个简单介绍。
7. 如何解决OOV问题
8. PCA和softmax的差别
9. LSTM与GRU区别
10. batchsize大或小有什么问题
11. SVM和LR各自的应用场景
svm是对于已知的样本做超平面进行分类,所以他的功能偏重于所给的样本分类;逻辑回归是一种极大似然估计的方式,是想通过一直样本推断未知类别的分类。
如果说样本有限,需要预测的样本并不会很多,推荐svm;如果说样本有限,需要预测的样本趋近于无穷,那么推荐逻辑回归。
另外逻辑回归是基于二维空间的特征分类,多用于二分类,而svm可以通过kenerl trick技术升维以做到多分类。
12. 常用的模型评估指标
1. 逻辑回归的损失函数
L = − ∑ i = 1 N [ y i l o g ( h θ ( x i ) ) + ( 1 − y i ) l o g ( 1 − h θ ( x i ) ) ] L = -\sum{_{i=1}^N}[ y_ilog(h_{\theta}(x_i))+(1-y_i)log(1-h_{\theta}(x_i))] L=−∑i=1N[yilog(hθ(xi))+(1−yi)log(1−hθ(xi))]
线性回归的损失函数是均方损失函数
2. 为什么逻辑回归为什么使用交叉熵损失不使用MSE
因为从反向求导来看,如果使用MSE, 那么导数中就涉及sigmoid项,那么当输入很大或者很小的时候,梯度都不会改变,所以不考虑使用MSE, 但是使用交叉熵损失等同于使用最大似然来学习一个由sigmoid参数化的Bernoulli分布。
3. SVM的损失函数是什么
合页损失函数
4. 核函数的种类
5. 介绍BERT、Transformer、Attention的原理及其作用,要通俗的解释
先说明Transformer是什么,在说明利用Transformer的encoder组成了BERT的特征抽取单元,再说一下什么是Attention机制,得到权重的步骤,然后讲一下self-attention和传统attention机制的区别。
attention的算法步骤:(详细见上面的总结)
- 首先再encoder端得到所有输入的隐藏层状态。
- 在decoder端的当前隐藏层的状态是 s t − 1 s_{t-1} st−1,我们使用 s t − 1 s_{t-1} st−1和encoder端得到的状态相乘得到输出矩阵,然后使用softmax函数对其进行处理就得到了一个权重矩阵,我们把这个权重矩阵对隐藏层状态的 h t h_t ht进行加权求和,然后利用decoder端的 s t − 1 s_{t-1} st−1,上一步的输出和当前的加权求和后的输出得到下一步的 s t s_t st。
6. 基础编程语言
Python问题:
7. word2vec的实现方式有哪些
8. Dropout原理与作用
在前向传播的时候,让某个神经元的激活值以一定的概率p停止工作,这样可以防止过拟合,使模型泛化性更强。
为什么说Dropout可以解决过拟合?
(1)取平均的作用: 先回到标准的模型即没有dropout,我们用相同的训练数据去训练5个不同的神经网络,一般会得到5个不同的结果,此时我们可以采用 “5个结果取均值”或者“多数取胜的投票策略”去决定最终结果。例如3个网络判断结果为数字9,那么很有可能真正的结果就是数字9,其它两个网络给出了错误结果。这种“综合起来取平均”的策略通常可以有效防止过拟合问题。因为不同的网络可能产生不同的过拟合,取平均则有可能让一些“相反的”拟合互相抵消。dropout掉不同的隐藏神经元就类似在训练不同的网络,随机删掉一半隐藏神经元导致网络结构已经不同,整个dropout过程就相当于对很多个不同的神经网络取平均。而不同的网络产生不同的过拟合,一些互为“反向”的拟合相互抵消就可以达到整体上减少过拟合。
(2)减少神经元之间复杂的共适应关系: 因为dropout程序导致两个神经元不一定每次都在一个dropout网络中出现。这样权值的更新不再依赖于有固定关系的隐含节点的共同作用,阻止了某些特征仅仅在其它特定特征下才有效果的情况 。迫使网络去学习更加鲁棒的特征 ,这些特征在其它的神经元的随机子集中也存在。换句话说假如我们的神经网络是在做出某种预测,它不应该对一些特定的线索片段太过敏感,即使丢失特定的线索,它也应该可以从众多其它线索中学习一些共同的特征。从这个角度看dropout就有点像L1,L2正则,减少权重使得网络对丢失特定神经元连接的鲁棒性提高。
(3)**Dropout类似于性别在生物进化中的角色:**物种为了生存往往会倾向于适应这种环境,环境突变则会导致物种难以做出及时反应,性别的出现可以繁衍出适应新环境的变种,有效的阻止过拟合,即避免环境改变时物种可能面临的灭绝。
总结:
- dropout类似于一个取平均的过程,由于dropout是随机屏蔽一些神经网络,这就相当于再训练不同的网络,当一个网络出现过拟合,另外一个逻辑上的网络有可能会得到一个欠拟合的模型,这样取平均就可以得到更好的效果。
- 减少了神经元之间复杂的共适应关系:因为两个神经元不一定每次都在相同的网络下出现,这就阻止了某个特征在特定的情况下才有作用的条件。迫使神经元提高自己的鲁棒性
9. 梯度消失梯度爆炸原因与解决方式
- 梯度消失和梯度爆炸
概念和表现:在反向求导的过程中,前面每层的梯度都是来自后面每层梯度的乘积,当层数过多时,有可能产生梯度不稳定,也就是梯度消失或者梯度爆炸,他门的本质都是因为梯度反向传播中的连乘效应。他的表现就是随着网络层数的加深,但是模型的效果却降低了。
梯度消失产生的原因: 隐藏层数量太大,使用了不合适的激活函数
**梯度爆炸产生的原因:**隐藏层数量太大,权重的初始化值过大,使用了不合适的激活函数
如何解决:
预训练加微调
加入正则化
梯度修剪
选择合适的激活函数,relu、leakrelu、elu等激活函数
batchnorm
Batchnorm本质上是解决反向传播过程中的梯度问题。batchnorm全名是batch normalization,简称BN,即批规范化,通过规范化操作把数据拉回到激活函数的梯度敏感区域,使得模型有一个更易于收敛。
LSTM
LSTM全称是长短期记忆网络(long-short term memory networks),是不那么容易发生梯度消失的,主要原因在于LSTM内部复杂的“门”(gates),如下图,LSTM通过它内部的“门”可以接下来更新的时候“记住”前几次训练的”残留记忆“,因此,经常用于生成文本中。
减少网络隐藏层的数量
选择合适的初始化手段
为什么ReLU可以避免梯度消失的问题
ReLU的正半轴是线性的,他的导数是1且是一个固定值,所以容易避免发生梯度消失和梯度爆炸的问题。但是他并不能从根本上解决梯度消失的问题,因为当输入是小于0的时候,就会把Relu的负半轴激活,在这一侧,ReLU的输出就是0,导数也是0, 他依旧无法避免梯度消失的问题。
10. 过拟合问题如何解决
定义: 过拟合就是模型在训练集上表现很好,能对训练数据充分拟合,误差也很小,但是在训练集上表现很差,泛化性不好。
11. word2vec是如何训练的
就是两种训练的方式 CBOW 和 skip-gram
12. 模型训练的停止标准是什么?如何确定模型的状态
停止的标准就是模型的指标不再上升,可以通过loss来观察模型的状态或者通过设置交叉验证集俩验证模型当前时刻的状态。
13. BERT细节,和GPT, ELMo的比较
BERT相对于GPT都是使用的Transformer的encoder端来作为网络内部的特征抽取器,但是GPT只是考虑了上文信息,类似于LSTM,但是BERT使用的上下问的饿信息,类似于BI-LSTM,对于ELMo,他内部使用的是LSTM来作为特征抽取器,并且ELMo相对于BERT的训练语料更少,参数量也更少。
14. Transformer结构,input_mask如何作用到后面self-attention计算过程
Transformer的encode中的层与层之间的链接是利用残差网路来进行连接的,因此输入可以直接作用于输出。呢么每个层之间都是使用残差来进行链接的,因此,input_mask就可以通过残差网络作用到后面的self-attention。
1. bert 为什么scale product(为什么要除以 d k \sqrt{d_k} dk)
https://zhuanlan.zhihu.com/p/149634836
向量的点积结果会很大,将softmax函数push到梯度很小的区域,scaled会缓解这种现象
2. transformer里encoder的什么部分输入给decoder
Decoder和Encoder是类似的,如下图所示,区别在于它多了一个Encoder-Decoder Attention层,这个层的输入除了来自Self-Attention之外还有Encoder最后一层的所有时刻的输出。Encoder-Decoder Attention层的Query来自下一层,而Key和Value则来自Encoder的输出。
3. MLM 为什么mask一部分保留一部分
添加mask相当于添加噪声,那么当模型进行预测时,并不知道对应的输入位置是不是正确的单词,这就需要更多的依赖上下文的信息进行预测,增加了输入的随机性,也增加了模型的纠错能力。
做题
自己实现sqrt函数,结果保留5位小数
10亿个数,内存只有1M,如何让这10亿个数有序
利用桶排序
1. BERT里面的三种embedding分别是什么,为什么要这样做?
- 为什么BERT有三个嵌入层
2. 如果要用树模型的话,可以做哪些特征工程?
n-gram,tf-idf,w2v
3. 假如说句子长度差别很大的话,tf-idf这个指标会有什么问题?one-hot encoding这个指标又会有什么问题
4. 介绍一下SVM,优化为什么要用对偶
支持向量机为一个二分类模型,它的基本模型定义为特征空间上的间隔最大的线性分类器。而它的学习策略为最大化分类间隔,最终可转化为凸二次规划问题求解。针对线性可分的问题,SVM并没有引入核函数,针对线性不可分的问题,SVM通过对偶性质引入和核函数来解决。这个时候SVM来进行分类的本质就是寻求一种使得可以在低纬度进行计算,但是相当于映射到高纬度上把不可分的数据集转换为可分的数据集。
引入对偶的目的主要是两个:
1)方便引入核函数;
2)原本模型的复杂度是和数据的维度有关,但是引入对偶问题以后,模型的复杂度只和变量的数量有关,这些变量就是支持向量。
5. Xgboost的应该着重调哪些参数
6. 讲一下训练词向量的方法
如何使用词向量生成句向量,可以是对每个句子的所有词向量取均值,来生成一个句子的vector
1. 词袋模型有哪些不足的地方
稀疏,无序,纬度爆炸,不能表达语义上的差别,每个词都是正交的,相当于每个词都没有关系。
2. word2vec的两种优化方法
层softmax技巧(hierarchical softmax)
解释一: 最后预测输出向量时候,大小是1*V 的向量,本质上是个多分类的问题。通过hierarchical softmax的技巧,把V分类的问题变成了log(V)次二分类。
解释二: 层次softmax的技巧是来对需要训练的参数的数目进行降低。所谓层次softmax实际上是在构建一个哈夫曼树,这里的哈夫曼树具体来说就是对于词频较高的词汇,它的树的深度就较浅,对于词频较低的单词的它的树深度就较大。
总结: 层次softmax就是利用一颗哈夫曼树来简化原来的softmax的计算量。具体来说就是对词频较高的单词,他在哈夫曼树上的位置就比较浅,而词频较低的位置就在树上的位置比较深。
负采样(negative sampling)
解释一: 本质上是对训练集进行了采样,从而减小了训练集的大小。每个词的概率由下式决定:
l e n ( w ) = c o u n t ( w ) 3 / 4 ∑ u ∈ v o c a b c o u n t ( u ) 3 / 4 len(w) = \frac{count(w)^{3/4}}{\sum\limits_{u \in vocab} count(u)^{3/4}} len(w)=u∈vocab∑count(u)3/4count(w)3/4
在训练每个样本时, 原始神经网络隐藏层权重的每次都会更新, 而负采样只挑选部分权重做小范围更新
解释二:
负采样主要解决的问题就是参数量过大,模型很难训练的问题。那么什么是负采样中的正例和负例?如果 vocabulary 大小为1万时, 当输入样本 ( “fox”, “quick”) 到神经网络时, “ fox” 经过 one-hot 编码,在输出层我们期望对应 “quick” 单词的那个神经元结点输出 1(这就是正例),其余 9999 个都应该输出 0(这就是负例)。在这里,这9999个我们期望输出为0的神经元结点所对应的单词我们称为 negative word. negative sampling 的想法也很直接 ,将随机选择一小部分的 negative words,比如选 10个 negative words 来更新对应的权重参数。
解释三:
Negative Sampling是对于给定的词,并生成其负采样词集合的一种策略,已知有一个词,这个词可以看做一个正例,而它的上下文词集可以看做是负例,但是负例的样本太多,而在语料库中,各个词出现的频率是不一样的,所以在采样时可以要求高频词选中的概率较大,低频词选中的概率较小,这样就转化为一个带权采样问题,大幅度提高了模型的性能。
3. word2vec的优缺点
优点
缺点
4. lstm和rnn有什么区别,解决了什么问题,lstm计算上是如何计算的,lstm输出的维度是怎么样的
lstm和rnn的区别
lstm的输出
他的输出就是一个 N*128 维的矩阵
1. 如何对句子进行编码
对每个token的词向量进行相加然后去平均值
2. 提取句子的特征向量,有哪几种方式
CNN,LSTM,Attention
1. LSTM和CNN有什么区别,都适用什么场景
LSTM
LSTM的主要是为了解决RNN中的长依赖问题和梯度消失问题。他的实现主要是依靠门机制来实现。内部包括三个门:遗忘门,输入门,输出门。对于遗忘门,主要是依靠sigmoid函数来实现一个抽象的门的功能,当sigmoid的输出为0时,相当于关闭门,上一时刻的隐藏层状态信息会全部遗忘掉; 当sigmoid的输出为1时,上一时刻的隐藏层状态信息会全部被用来更新cell状态。在输入门中通过sigmoid函数结合当前输入和上一时刻的状态输出来决定更新cell中的信息。从宏观上来看,LSTM中有一个cell单元贯穿始终,使得中间状态信息直接向后传播。因此这使得LSTM可以有效的解决长依赖问题。但是LSTM由于隐藏层状态的计算和上一时刻的状态有关,因此无法实现并行计算。
CNN
对于CNN,主要是由卷积层,池化层和全连接层来实现。原始的CNN,由于卷积核的限制,他也相当于处理一个N-gram问题,依旧无法解决长依赖的问题。但是随着Dilater CNN的出现,打破了原有的卷积核形式,可以使得卷积核可以以类似于跳一跳的形似提取前后文本序列的相关特征。但是由于CNN本身各个卷积核之间互不干扰,因此可以完美的实现并行计算。
2. xgboost 和 gbdt 区别 , gbdt 具体怎么实现的,具体讲一下,(比如说现在已经构建好了1 2 棵树 那么第三棵树如何构建 如何选择特征,具体说明)
区别
GBDT的具体实现过程
3. cbow skipgram 具体说说区别, 负采样,层级softmax 原理 具体说一下
4. 负采样
负采样主要是为了应对参数量太多,计算量太大的问题提出的一种解决方案。因为字典是非常大的,比如一万个,那么每次都需要对着一万个单词进行计算,那么反向传播的计算量是非常大的。但是我们可以只随机选择频率比较高的单词作为负样本进行反向传播,那么这个计算量就比较小了。
5. 层次softmax
这个主要是利用哈夫曼树的原理进行层次softmax。
1. GBDT和RF哪个树比较深
RF深。说了boost和bagging的思想。boost使用低方差学习器去拟合偏差,所以XBG和LGB有树深的参数设置,RF是拟合方差,对样本切对特征切,构造多样性样本集合,每棵树甚至不剪枝。
2. XGB是如何判断特征的重要性的
gain 增益意味着相应的特征对通过对模型中的每个树采取每个特征的贡献而计算出的模型的相对贡献。与其他特征相比,此度量值的较高值意味着它对于生成预测更为重要。
cover 覆盖度量指的是与此功能相关的观测的相对数量。例如,如果您有100个观察值,4个特征和3棵树,并且假设特征1分别用于决策树1,树2和树3中10个,5个和2个观察值的叶结点;那么该度量将计算此功能的覆盖范围为10+5+2 = 17个观测值。这将针对所有决策树结点进行计算,并将17个结点占总的百分比表示所有功能的覆盖指标。
**freq 频率(频率)**是表示特定特征在模型树中发生的相对次数的百分比。在上面的例子中,如果feature1发生在2个分裂中,1个分裂和3个分裂在每个树1,树2和树3中;那么特征1的权重将是2 1 3 = 6。特征1的频率被计算为其在所有特征的权重上的百分比权重。)
1. L1、L2不同?L1为什么能稀疏?
从数学分布讲了,L1是拉普拉斯分布, L2 是高斯分布;讲了图解为什么L1能稀疏,一个圈一个菱形,容易交在轴上。工程上讲了,L1的近似求导,区间内0区间外优化。然后L2是直接求导比较简单。
1. C4.5相对ID3决策树的优点
两者主要是结点分裂的计算标准不相同,ID3是使用的信息增益作为结点分类标准,而C4.5是使用信息增益比。
ID3算法以信息增益为准则来选择决策树划分属性。值多的属性更有可能会带来更高的纯度提升,所以信息增益的比较偏向选择取值多的属性。所以为了解决这个问题就用了信息增益比。C4.5算法并不是直接选择增益率最大的候选划分属性,而是使用了一个启发式:先从候选划分属性中找出信息增益高于平均水平的属性,再从中选择增益率最高的。
2. LSTM如何解决RNN存在的问题的?lstm的激活函数可以用relu吗?
- 理解RNN梯度消失和弥散以及LSTM为什么能解决
- LSTM是如何解决梯度消失和梯度报站的问题的
RNN 存在梯度爆炸的根源
RNN的主要问题主要是在处理长距离信息的时候存在梯度消失或者梯度爆炸的问题,主要是由于在反向传播的过程中,导数小于1,累乘就会得到一个比较小的数,产生梯度消失。那么在反向传播的计算公式中还存在参数W,如果这个数值太大,经过梯度的累乘就会导致梯度爆炸的问题。
为什么不把RNN中的tanh激活函数换为ReLU?
ReLU可以在一定程度上缓解梯度消失的问题,但是有ReLU会导致非常大的输出,,最后的结果会变成多个W参数连乘,如果W中存在特征值>1,那么经过反向传播的连乘后就会产生梯度爆炸,RNN仍然无法传递较远的距离
RNN中的梯度消失和梯度爆炸
RNN 中的梯度消失/梯度爆炸和普通的 MLP 或者深层 CNN 中梯度消失/梯度爆炸的含义不一样。MLP/CNN 中不同的层有不同的参数,各是各的梯度;而 RNN 中同样的权重在各个时间步共享,最终的梯度 g = 各个时间步的梯度 g_t 的和。RNN 中总的梯度是不会消失的。即便梯度越传越弱,那也只是远距离的梯度消失,由于近距离的梯度不会消失,所有梯度之和便不会消失。RNN 所谓梯度消失的真正含义是,梯度被近距离梯度主导,导致模型难以学到远距离的依赖关系。
LSTM解决梯度消失的本质方法
把返乡传播中的连乘变成了相加的形式,这就使得每一部分的梯度的权重相同,不容易产生梯度消失的问题,但是因为是相加,有可能会产生梯度爆炸的问题
RNN主要存在的问题是无法解决问题,并且存在梯度消失或者梯度爆炸的现象。产生这种现象的根源不主要是
1. LSTM与RNN的区别
LSTM主要是结构上的不同,RNN和LSTM都可以把之前得到的隐藏层状态加入到当前隐藏层状态的生成中,但是
RNN内部是使用一个简单的加权求和并添加一层tanh激活函数来得到当前时刻的隐藏层状态,并且随着序列的加长,起始的隐藏层状态在后续中会产生较大的衰减,也就是无法应对长依赖问题,也容易发生梯度消失的问题。
LSTM的内部机制主要是三个门结构来决定之前隐藏层状态对当前时刻要生成的隐藏层状态的影响。并且从宏观上来看,LSTM前后有一个cell状态贯穿始终,能够比较好的解决长依赖的问题。
2. 梯度消失/爆炸的原因及解决方法
概念和表现:在反向求导的过程中,前面每层的梯度都是来自后面每层梯度的乘积,当层数过多时,有可能产生梯度不稳定,也就是梯度消失或者梯度爆炸,他门的本质都是因为梯度反向传播中的连乘效应。他的表现就是随着网络层数的加深,但是模型的效果却降低了。
梯度消失产生的原因: 隐藏层数量太大,使用了不合适的激活函数
**梯度爆炸产生的原因:**隐藏层数量太大,权重的初始化值过大,使用了不合适的激活函数
如何解决:
预训练加微调
加入正则化
梯度修剪
选择合适的激活函数,relu、leakrelu、elu等激活函数
batchnorm
Batchnorm本质上是解决反向传播过程中的梯度问题。batchnorm全名是batch normalization,简称BN,即批规范化,通过规范化操作把数据拉回到激活函数的梯度敏感区域,使得模型有一个更易于收敛。
LSTM
LSTM全称是长短期记忆网络(long-short term memory networks),是不那么容易发生梯度消失的,主要原因在于LSTM内部复杂的“门”(gates),如下图,LSTM通过它内部的“门”可以接下来更新的时候“记住”前几次训练的”残留记忆“,因此,经常用于生成文本中。
选择合适的权重初始化手段
为什么ReLU可以避免梯度消失的问题
ReLU的正半轴是线性的,他的导数是1且是一个固定值,所以容易避免发生梯度消失和梯度爆炸的问题。但是他并不能凶根本上解决梯度消失的问题,因为当输入是小于0的时候,就会把Relu的负半轴激活,在这一侧,ReLU的输出就是0,导数也是0, 他依旧无法避免梯度消失的问题。
3. transform 的mask到底有什么作用
mask的作用相当于在输入侧引入噪声。在模型训练的过程中,模型并不知道输入的当前位置是否正确,那么久需要更多的依赖上下文去预测当前位置的正确性,一定程度上增加了训练数据的不确定性,另一方面也增加了模型的纠错能力。
4. lstm门到底那个门更新细胞状态
输入门对细胞状态进行更新(遗忘门的作用只是计算上一个状态的信息有多少在当前时刻得到保留并对此刻产生的隐藏层状态有多少影响)
5. 如何用word2vec的方式构造sentence2vec
6. 数据归一化的好处
数据归一化后**,最优解的寻优过程明显会变得平缓,更容易正确的收敛到最优解**。
7. 每次训练LSTM的权重是一样的吗?
参数是共享的,每一个时刻,用的都是同一个参数矩阵
8. 常见激活函数以及优缺点
sigmoid函数
优点:
缺点:
sigmoid函数在变量取绝对值非常大的正值或负值时会出现饱和现象,意味着函数会变得很平,并且对输入的微小改变会变得不敏感。在反向传播时,当梯度接近于0,权重基本不会更新,很容易就会出现梯度消失的情况,从而无法完成深层网络的训练。
sigmoid函数的输出不是0均值的,会导致后层的神经元的输入是非0均值的信号,这会对梯度产生影响。
计算复杂度高,因为sigmoid函数是指数形式。
tanh函数
Tanh函数是 0 均值的,因此实际应用中 Tanh 会比 sigmoid 更好。但是仍然存在梯度饱和与exp计算的问题。
ReLU函数
优点:
使用ReLU的SGD算法的收敛速度比 sigmoid 和 tanh 快。
在x>0区域上,不会出现梯度饱和、梯度消失的问题。
计算复杂度低,不需要进行指数运算,只要一个阈值就可以得到激活值。
缺点:
ReLU的输出不是0均值的。
Dead ReLU Problem(神经元坏死现象):ReLU在负数区域被kill的现象叫做dead relu。ReLU在训练的时很“脆弱”。在x<0时,梯度为0。这个神经元及之后的神经元梯度永远为0,不再对任何数据有所响应,导致相应参数永远不会被更新。
产生这种现象的两个原因:参数初始化问题;learning rate太高导致在训练过程中参数更新太大。
解决方法:采用Xavier初始化方法,以及避免将learning rate设置太大或使用adagrad等自动调节learning rate的算法。
9. 数据不平衡怎么处理?
1. Transformer中涉及的几个公式写一下
1)multi-head attention层 A t t e n t i o n ( Z ) = s o f t m a x ( Q ∗ K T d k ∗ V ) Attention(Z)=softmax(\frac{Q*K^T}{\sqrt{d_k}}*V) Attention(Z)=softmax(dkQ∗KT∗V)
2)add&normal层 L a y e r N o r m a l ( x + S u b l a y e r ( x ) ) LayerNormal(x+Sublayer(x)) LayerNormal(x+Sublayer(x))
3)FFNN层 F F N N ( x ) = m a x ( 0 , x W 1 + b 1 ) W 2 + b 2 FFNN(x)=max(0, xW_1+b_1)W_2+b_2 FFNN(x)=max(0,xW1+b1)W2+b2
1. encoder包含几个大块
2. 马尔科夫决策过程
2.1 马尔科夫过程:
马尔可夫过程即为具有马尔可夫性的过程,即过程的条件概率仅仅与系统的当前状态相关,而与它的过去历史或未来状态都是独立、不相关的。
2.2 马尔科夫决策过程:
马尔可夫决策过程(Markov Decision Process,MDP)是带有决策的MRP,其可以由一个五元组构成 。
我们讨论的MDP一般指有限(离散)马尔可夫决策过程。
1)策略
策略(Policy)是给定状态下的动作概率分布,即: π ( a ∣ s ) = P [ A t = a ∣ S t = a ] \pi(a|s)=P[A_t=a|S_t=a] π(a∣s)=P[At=a∣St=a]
3. 讲一下决策树
决策树就是if-else模型,他的主要步骤是寻找最优划分特征把当前节点分裂。那么针对每一个结点力的数据,如果采用的是ID3方法,那就计算他们的信息增益,选择最小的作为分类特征进行分裂,如果是C4.5就计算信息增益比来选择分裂的特征值。对于CART回归树是计算它的信息基尼系数来寻找最优的分裂特征和分裂特征值。
4. attention的计算过程
5. 随机森林的随机性体现在什么地方
1.如何解决过拟合问题,尽可能全面?(几乎每次都被问到)
定义: 过拟合就是模型在训练集上表现很好,能对训练数据充分拟合,误差也很小,但是在训练集上表现很差,泛化性不好。
解决方案:
2.如何判断一个特征是否重要?
3.有效的特征工程有哪些?
4.数学角度解释一下L1和L2正则化项的区别?
- L1和L2的直观理解
直观的区别就是计算的方式不一样,L1正则就是在loss function后面加上模型参数的一范数,L2正则就是加上2范数。
L1正则化可以产生稀疏权值矩阵,即产生一个稀疏模型,可以用于特征选择
L2正则化可以防止模型过拟合(overfitting);一定程度上,L1也可以防止过拟合
5.注意力机制,self-attention ?
6.有哪些embedding 方法?
7.word2vec中,为啥词义相近的两个词,embedding向量越靠近?
9.GBDT中的“梯度提升”,怎么理解?和“梯度下降”有啥异同?
10.常见的降维方法?PCA和神经网络的embedding降维有啥区别?
11.图卷积神经网络了解吗?(这里感谢滴滴面试官的提问,确实是我的盲点)
12.Bert为啥表现好?
13.SVM用折页损失函数有啥好处?
14.什么是交叉熵,为什么逻辑回归要用交叉熵作为损失函数?
为什么分类问题不适用均方误差,回归问题不适用交叉熵?
- 线性回归如果使用交叉熵损失函数,在训练的时候反向传播求导时的导数非常小,这将导致w,b的梯度不产生变化,也就是梯度消失现象,但是在使用平方误差时不会产生上述问题。
在分类问题中,我们希望模型学到的数据分布跟真实分布一致。但是我们无法得到真实分布,只能假设训练集的分布与真实的分布相近。我们希望模型尽可能拟合训练集的分布。衡量两个分布之间的不同一般用的是KL散度。最小化两个分布之间的不同相当于使KL散度最小。KL散度等于交叉熵减去熵。对于一个已知的训练集,它的熵是确定的。所以优化KL散度等价于优化交叉熵。而且交叉熵的计算更加简单。
逻辑回归使用交叉熵作为损失函数而不用平方损失,一是因为使用交叉熵可以保证目标函数是凸函数,使用平方损失无法保证;二是使用平方损失的话会出现当输出值接近1或者0的时候,梯度非常小,不容易学习。
15.XGboost 的节点分裂时候,依靠什么?数学形式?XGboost 比GBDT好在哪?
17.除了梯度下降,还有啥优化方法?为啥不用牛顿法呢?
18.skip gram和CBOW训练过程中有啥区别?谁更好?
可以看出,skip-gram进行预测的次数是要多于cbow的:因为每个词在作为中心词时,都要使用周围词进行预测一次。这样相当于比cbow的方法多进行了K次(假设K为窗口大小),因此时间的复杂度为O(KV),训练时间要比cbow要长。
但是在skip-gram当中,每个词都要收到周围的词的影响,每个词在作为中心词的时候,都要进行K次的预测、调整。因此, 当数据量较少,或者词为生僻词出现次数较少时, 这种多次的调整会使得词向量相对的更加准确。因为尽管cbow从另外一个角度来说,某个词也是会受到多次周围词的影响(多次将其包含在内的窗口移动),进行词向量的跳帧,但是他的调整是跟周围的词一起调整的,grad的值会平均分到该词上, 相当于该生僻词没有收到专门的训练,它只是沾了周围词的光而已。
21.SVM都能用核函数,逻辑回归咋不用呢?
核方法用于分类的时候用的是hinge loss,可以方便的转化为对偶形式求解,也就是SVM。
逻辑回归中交叉熵这个损失函数,对kernel methods来说可能有点伤…转化易求解的形式比较难,而且损失是不是凹函数都不一定。
24.常见的采样方法?
25.如何解决样本不均衡问题?
1.如何解决样本不均衡的问题
上采样(过采样)
上采样方法通过增加分类中少数类样本的数量来实现样本均衡,最直接的方法是简单复制少数类样本形成多条记录,这种方法的缺点是如果样本特征少而可能导致过拟合的问题;经过改进的过抽样方法通过在少数类中加入随机噪声、干扰数据或通过一定规则产生新的合成样本
下采样(欠采样)
下采样方法通过减少分类中多数类样本的样本数量来实现样本均衡,最直接的方法是随机地去掉一些多数类样本来减小多数类的规模,缺点是会丢失多数类样本中的一些重要信息。
通过正负样本的惩罚权重解决样本不均衡
对于分类中不同样本数量的类别分别赋予不同的权重(一般思路分类中的小样本量类别权重高,大样本量类别权重低),然后进行计算和建模。
组合/集成方法
例如,在数据集中的正、负例的样本分别为100和10000条,比例为1:100。此时可以将负例样本(类别中的大量样本集)随机分为100份(当然也可以分更多),每份100条数据;然后每次形成训练集时使用所有的正样本(100条)和随机抽取的负样本(100条)形成新的数据集。如此反复可以得到100个训练集和对应的训练模型。
通过特征选择解决样本不均衡
一般情况下,样本不均衡也会导致特征分布不均衡,但如果小类别样本量具有一定的规模,那么意味着其特征值的分布较为均匀,可通过选择具有显著型的特征配合参与解决样本不均衡问题,也能在一定程度上提高模型效果。
26.高维稀疏特征为啥不适合神经网络训练?
// 利用动态规划来做
#include
using namespace std;
// 做法一
class Solution{
public:
int getMaxSumOfSubSeq(vector &nums){
int len=nums.size();
vector dp(len+1, 0);
dp[0] = nums[0];
int sum=nums[0];
for(int i=1; i=0)
sum+=nums[i];
else
sum=nums[i];
dp[i] = max(sum, dp[i-1]);
}
return dp[len];
}
};
// 做法二
class Solution{
public:
int maxSubArray(vector &nums){
int pre=0;
int maxAns=nums[0];
for(int i=0; i
// 递归或中序遍历后看是否为递增序列
#include
using namespace std;
void inOrder(BinaryTree* root, vector &res){
BinaryTree * pNode = root;
if(root==nullptr){
return res;
}
if(pNode->pLeft != nullptr){
inOrder(pNode->pLeft, res);
}
res.push(pNode->valus);
if(pNode->pRight != nullptr){
inOrder(pNode->pRight, res);
}
}
int main(){
vector res;
inorder(root, res);
for(int i=1; i
// 剑指offer上的一个题
// 利用快慢指针来做
#include
using namespace std;
// 先找到相遇的结点
ListNode* Meeting(ListNode* pHead){
if(pHead==nullptr) return nullptr;
ListNode* pSlow = pHead;
ListNode* pFast = pHead->next;
while(pFast != nullptr && pSlow != nullptr){
if(pFast == pSlow) return pFast;
pSlow = pSlow->next;
pFast = pFast->next;
if(pFast->next != nullptr){
pFast = pFast->next;
}
}
return nullptr;
}
// 先统计环中的结点个数,再利用快慢指针寻找入口点
ListNode* EntryNodeOfLoop(ListNode* pHead){
ListNode* meetingNode = Meeting(pHead);
if(meetingNode == nullptr) return nullptr;
ListNode* pNode = meetingNode;
int countor = 1;
pNode = pNode->next;
while(pNode != meetingNode){
countor++;
pNode = pNode->next;
}
ListNode* pFast = pHead;
ListNode* pSlow = pHead;
for(int i=0; inext;
}
while(pFast != pSlow){
pFast = pFast->next;
pSlow = pSlow->next;
}
return pFast;
}
int main(){
}
// 可以使用一个map记录所有可能的数的组合
#include
using namespace std;
int getSumTopK(vector &arr1, vector &arr2, int k){
map hash;
for(int i=0; i::iterator iter = hash.begin(); // map的遍历
// auto iter = hash.begin(); // 这也可以作为map的遍历
int res = 0;
while(k>=0 && iter!=hash.end()){
if(k<=iter->second){
return iter->first;
}
k-=iter->second;
iter++;
}
return 0;
}
int main(){
vector arr1={1,2,3};
vector arr2={2,3,5};
int res = getSumTopK(arr1, arr2, 8);
cout << res << endl;
return 0;
}
// 利用队列来做并且设定好两个记录变量m和n,只要队列不为空就一直打印
class Solution{
public:
void PrintFromTopToBottom(BinaryTreeNode* pTreeRoot){
if(!pTreeRoot) return;
deque dequeTreeNode;
dequeTreeNode.push_back(pTreeRoot);
while(dequeTreeNode.size()){
BinaryTreeNode *pNode = dequeTreeNode.front();
dequeTreeNode.pop_front();
cout << pNode->m_nValue << endl;
if(pNode->m_pLeft != nullptr){
dequeTreeNode.push_back(pNode->m_pLeft);
}
if(pNode->m_pRight != nullptr){
dequeTreeNode.push_back(pNode->m_pRight);
}
}
}
};
// 如果不是排序数组就先排一下序
// 每一个数都有可能是一个根节点
class Solution {
public:
int numTrees(vector &nums) {
int n=nums.size();
vector dp(n + 1, 0); // dp[i]代表i个数字有多少个排列方式
dp[0] = dp[1] = 1;
for(int i = 2; i <= n; ++i)
{
for(int j = 1; j <= i; ++j)
{
//G(i) += G(j - 1) * G(n - j)
dp[i] += dp[j - 1] * dp[i - j]; // 为什么j从1开始遍历,因为要拿出来一个数字作为根节点
}
}
return dp[n];
}
};
// 递归版本的遍历
class Solution{
public:
void Inorder(BinaryTree* root){
if(root == NULL) return NULL;
Inorder(root->left);
cout << root->val << endl;
Inorder(root->right);
}
};
// 非递归版本
void InOrderWithoutRecursion1(BTNode* root)
{
//空树
if (root == NULL)
return;
//树非空
BTNode* p = root;
stack s;
while (!s.empty() || p)
{
//一直遍历到左子树最下边,边遍历边保存根节点到栈中
while (p)
{
s.push(p);
p = p->lchild;
}
//当p为空时,说明已经到达左子树最下边,这时需要出栈了
if (!s.empty())
{
p = s.top();
s.pop();
cout << setw(4) << p->data;
//进入右子树,开始新的一轮左子树遍历(这是递归的自我实现)
p = p->rchild;
}
}
}
// 首先重构二叉树,然后递归得到后序遍历
class Solution {
public:
TreeNode* buildTree(vector& preorder, vector& inorder) {
int pres=0,ins=0;
int pree=int(preorder.size()-1),ine=int(inorder.size()-1);
return myBT(inorder,preorder,pres,pree,ins,ine);
}
TreeNode* myBT(vector& inorder, vector& preorder,int pres, int pree, int ins, int ine){
TreeNode* root;int mid;
if(pres > pree || ins > ine )
return NULL;
root = new TreeNode(preorder[pres]);
for(int i=0;ileft = myBT(inorder,preorder,pres+1,pres+mid-ins,ins,mid-1);
root->right = myBT(inorder,preorder,pree-(ine-mid)+1,pree,mid+1,ine);
return root;
}
};
// 后序遍历,递归版本
void laterOrder(TreeNode* root){
if(root ==NULL) return;
laterOrder(root->left);
laterOrder(root->right);
cout << root->val << endl;
}
// DP来做(也可以使用暴力解来做)
class Solution {
public:
int lengthOfLIS(vector& nums) {
int n=(int)nums.size();
if (n == 0) return 0;
vector dp(n, 0);
for (int i = 0; i < n; ++i) {
dp[i] = 1;
for (int j = 0; j < i; ++j) {
if (nums[j] < nums[i]) {
dp[i] = max(dp[i], dp[j] + 1);
}
}
}
return *max_element(dp.begin(), dp.end());
}
};
10.累加数 Leetcode 306
// 如果是二叉搜索树
class Solution {
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q){
if (root == NULL) return root;
while(root!=NULL){
if(root->val > p->val && root->val > q->val){
root = root->left;
}
else if(root->val < p->val && root->val < q->val){
root = root->right;
}
else{
break;
}
}
return root;
}
};
// 如果是普通二叉树。我们需要知道两个结点所在的路径,然后去搜索公共结点
class Solution{
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q){
if (root == nullptr){
return nullptr;
}
vector v1; // 保存p的路径
vector v2; // 保存q的路径
TreeNode* res = nullptr;
bool flag1 = findPath(root, p, v1);
bool flag2 = findPath(root, q, v2);
if(flag1 == true && flag2 == true){
int i=0, j=0;
while(i &path){
bool flag = false;
path.push_back(root);
cout << "push_back: " << root->val << endl;
if(root == target){
cout << "have found" << endl;
return true;
}
if(root->left != nullptr && flag == false){
flag = findPath(root->left, target, path);
}
if(root->right != nullptr && flag == false){
flag = findPath(root->right, target, path);
}
// 什么时候弹出去
if(flag == false){
cout << "have poped: " << root->val << endl;
path.pop_back();
}
return flag;
}
};
// 这个题可以使用快排的思想来做
class Solution {
public:
int findKthLargest(vector& nums, int k) {
sort(nums.begin(), nums.end());
int current = nums[0];
for(int i=nums.size()-1; i>=0 && k>0;i--){
current = nums[i];
k--;
}
return current;
}
};
// 利用堆来实现
class Solution2{
public:
int findKthLargest(vector& nums, int k) {
priority_queue, greater> pq; // 升序
// priority_queue ,less >q; // 降序
for (auto n : nums) {
if (pq.size() == k && pq.top() >= n) {
cout << "pq.top()" << pq.top() << endl;
continue;
}
if (pq.size() == k) {
cout << "pq.pop()elements: " << pq.top() << endl;
pq.pop();
}
pq.push(n);
}
return pq.top();
}
};
// 利用递归+快速幂来做
class Solution{
public:
double myPow(double x, int n) {
double res = 1;
if(n==0) return 1;
if(n==1) return x;
if(n==-1) return 1.0/x;
long num = n;
if(n<0) num=-num;
res = myPow(x*x, num/2);
if(num%2==1){
res*=x;
}
return n>0?res:(1.0/res);
}
};
// 利用map来做
#include
using namespace std;
class Solution{
public:
int lengthOfLongestSubstring(string s){
map hash;
int start=0;
int end=0;
int res=0;
int len = s.size();
while(start
// 利用递归的思想来做
class Solution {
public:
bool helper(TreeNode* A, TreeNode* B) { // 判断两个树是否完全一样
if (A == NULL || B == NULL) {
return B == NULL ? true : false;
}
if (A->val != B->val) {
return false;
}
return helper(A->left, B->left) && helper(A->right, B->right);
}
bool isSubStructure(TreeNode* A, TreeNode* B) {
if (A == NULL || B == NULL) {
return false;
}
return helper(A, B) || isSubStructure(A->left, B) || isSubStructure(A->right, B);
}
};
// 利用栈和队列来做
// 利用队列和栈来实现
class Solution {
public:
vector> levelOrder(TreeNode* root) {
vector> res;
if(root==NULL) return res;
int m=1, n=0,flag=0;
TreeNode* pNode=NULL;
queue treeQueue;
stack treeStack;
treeQueue.push(root);
while (!treeQueue.empty())
{
n=m;
m=0;
vector mid;
if(flag==0){
for(int i=0; ival);
if(pNode->left!=NULL){
treeQueue.push(pNode->left);
m++;
}
if(pNode->right != NULL){
treeQueue.push(pNode->right);
m++;
}
treeQueue.pop();
}
res.push_back(mid);
flag=1;
}
else{
for(int i=0; ileft!=NULL){
treeQueue.push(pNode->left);
m++;
}
if(pNode->right!=NULL){
treeQueue.push(pNode->right);
m++;
}
treeQueue.pop();
}
while (!treeStack.empty()) // 打印栈内的数值
{
pNode=treeStack.top();
mid.push_back(pNode->val);
treeStack.pop();
}
res.push_back(mid);
flag=0;
}
}
return res;
}
};
class Solution {
public:
vector> findContinuousSequence(vector &nums, int target) {
vector> res;
vector mid;
int len=nums.size();
if(len==0) return res;
findCore(nums, target, res, mid, 0, 0);
return res;
}
void findCore(vector &nums, int target, vector> &res, vector &mid, int curSum, int i){
if(curSum==target){
res.push_back(mid);
}
if(i==nums.size()) return;
for(int j=i; j
#include
using namespace std;
class Solution {
public:
int rob(vector& nums) {
int len = nums.size();
if(len==0){return 0;}
vector maxprofit(len+1, 0);
maxprofit[0] = 0;
maxprofit[1] = nums[0];
for(int i=2; i<=len; i++){
maxprofit[i] = max(maxprofit[i-1], maxprofit[i-2]+nums[i-1]);
}
return maxprofit[len];
}
};
#include
using namespace std;
vector bubbleSort(vector &datas){
int len = datas.size();
for(int i=0; i datas[j+1]){
int temp = datas[j];
datas[j] = datas[j+1];
datas[j+1] = temp;
}
}
}
return datas;
}
// 递归版本
void postOrder1(BinTree *root) {
if(root == NULL) return;
else{
postOrder1(root->left);
postOrder1(root->right);
cout << root->val << endl;
}
}
// 非递归版本
void postOrder2(BinTree *root) //非递归后序遍历
{
stack s;
BinTree *p=root;
BTNode *temp;
while(p!=NULL||!s.empty())
{
while(p!=NULL) //沿左子树一直往下搜索,直至出现没有左子树的结点
{
BTNode *btn=(BTNode *)malloc(sizeof(BTNode));
btn->btnode=p;
btn->isFirst=true;
s.push(btn);
p=p->lchild;
}
if(!s.empty())
{
temp=s.top();
s.pop();
if(temp->isFirst==true) //表示是第一次出现在栈顶
{
temp->isFirst=false;
s.push(temp);
p=temp->btnode->rchild;
}
else//第二次出现在栈顶
{
cout<btnode->data<<"";
p=NULL;
}
}
}
}
// 采用动态规划来做
// 最长公共子序列
#include
using namespace std;
class SolutionMaxSubSequence{
public:
// 递归
int dp(string str1, string str2, int i, int j){
if(i==-1 || j==-1){
return 0;
}
else if(str1[i] == str2[j]){
return dp(str1, str2, i-1, j-1)+1;
}
else
{
return max(dp(str1, str2, i-1, j), dp(str1, str2, i, j-1));
}
}
int mian_(string s1, string s2){
if(s1==s2) return s1.size();
int len1 = s1.size();
int len2 = s2.size();
if(len1==0 || len2==0){
return 0;
}
return dp(s1, s2, len1-1, len2-2);
}
// 转化为动态规划问题
int mainDP(string s1, string s2){
if(s1==s2) return s1.size();
int len1 = s1.size();
int len2 = s2.size();
vector > dp(len1+1, vector(len2+1, 0));
for(int i=1; i<=len1; i++){
for(int j=1; j<=len2; j++){
if(s1[i-1] == s2[j-1]){
dp[i][j] = 1+dp[i-1][j-1];
}
else{
dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
}
}
}
return dp[len1][len2];
}
};
// 基于递归的方法
class BinaryTreeMaxPath{
public:
int getRes(TreeNode* root){
if(root==NULL) return 0;
int l = getRes(root->left)+1;
int r = getRes(root->right)+1;
int maxDepth = l>r?l:r;
return maxDepth;
}
};
// 求最长路径长度也可以基于层序遍历来求最大深度
class Solution {
public:
int minDepth(TreeNode* root) {
if(root==NULL) return 0;
queue q;
q.push(root);
int depth = 1;
while (!q.empty()) // 当我们的队列不为空的时候
{
int qsize = q.size();
for(int i=0; ileft != NULL){ // 把pNode的相邻结点全部加入到队列
q.push(pNode->left);
}
if(pNode->right != NULL){
q.push(pNode->right);
}
q.pop();
}
depth++; // 增加步数
}
return depth;
}
};
// 拓展题目,求最大路径和
class Solution {
public:
int res=INT_MIN; // 全局变量
int maxPathSum(TreeNode* root) {
getmax(root);
return res;
}
int getmax(TreeNode* root){
if(!root)
return 0;
// 计算左边分支最大值,左边分支如果为负数还不如不选择
int left=max(getmax(root->left),0);
// 计算右边分支最大值,右边分支如果为负数还不如不选择
int right=max(getmax(root->right),0);
// left->root->right 作为路径与历史最大值做比较
res=max(res,root->val+left+right);
// 返回经过root的单边最大分支给上游
return max(left,right)+root->val;
}
};
//直接reverse就可以反转字符串
#include
using namespace std;
class ReverseString{
public:
string reverseString(string s){
int len = s.size();
if(len==1) return s;
string res = "", temp="";
for(int i=0; i
class Solution{
public:
void swap(vector &a, int i, int j){
int temp = 0;
temp = a[i];
a[i] = a[j];
a[j] = temp
}
void save(vector &a, int q){
for(int i=0; i&a, int p, int q){
if(p==q){
save(a, q+1);
}
else{
for(int i=p; i<=q; i++){
swap(a, i, p);
main_(a, p+1, q);
swap(a, i, p);
}
}
}
};
// 利用队列和栈来实现
class Solution {
public:
vector> levelOrder(TreeNode* root) {
vector> res;
if(root==NULL) return res;
int m=1, n=0,flag=0; // flag是正反序打印的标志位
TreeNode* pNode=NULL;
queue treeQueue;
stack treeStack;
treeQueue.push(root);
while (!treeQueue.empty())
{
n=m;
m=0;
vector mid;
if(flag==0){
for(int i=0; ival);
if(pNode->left!=NULL){
treeQueue.push(pNode->left);
m++;
}
if(pNode->right != NULL){
treeQueue.push(pNode->right);
m++;
}
treeQueue.pop();
}
res.push_back(mid);
flag=1;
}
else{
for(int i=0; ileft!=NULL){
treeQueue.push(pNode->left);
m++;
}
if(pNode->right!=NULL){
treeQueue.push(pNode->right);
m++;
}
treeQueue.pop();
}
while (!treeStack.empty())
{
pNode=treeStack.top();
mid.push_back(pNode->val);
treeStack.pop();
}
res.push_back(mid);
flag=0;
}
}
return res;
}
};
class Solution {
public:
vector spiralOrder(vector>& matrix) {
vector ans;
int R,C;
if(!(R=matrix.size()) || !(C=matrix[0].size())){
return ans;
}
int top=0,left=0,right=C-1,bottom=R-1;
while(ans.size() < R*C){
//遍历上边
for(int i=left;i<=right;++i) ans.push_back(matrix[top][i]);
//遍历右边
for(int i=top+1;i=left && bottom > top;--i) ans.push_back(matrix[bottom][i]);
//遍历左边
for(int i=bottom-1;i>top && left < right;--i) ans.push_back(matrix[i][left]);
top++;bottom--;
left++;right--;
}
return ans;
}
};
// 按照数学推导直接计算
class Solution {
public:
int mySqrt(int x) {
if (x == 0) {
return 0;
}
int ans = exp(0.5 * log(x)); // x=e^{1/2 * log(x)}
return ((long long)(ans + 1) * (ans + 1) <= x ? ans + 1 : ans); // 取整
}
};
// 二分查找
double getSqrt(int x,double precision) {
double left = 0, right = x;
while (1) {
double mid = left + (right - left) / 2;
if (abs(x /mid - mid) < precision)
return mid;
else if (x / mid > mid)
left = mid + 1;
else
right = mid - 1;
}
}
class Solution { // 快速幂
public:
double myPow(double x, int n) {
if(x == 1 || n == 0) return 1;
double ans = 1;
long num = n;
if(n < 0){
num = -num;
x = 1/x;
}
while(num){
if(num & 1) ans *= x;
x *= x;
num >>= 1;
}
return ans;
}
};
字符串分割,多个分隔符(前缀树)
字符串匹配问题
有足够多的数据(内存无法一次性装下),如何获得最大的k个数
给一个无序数组,输出最小的不在数组中的正数
数组分为两部分,使得他们和的差值最小
两颗二叉树合并
多个字符串,给定前缀和长度比例阈值,返回符合条件的字符串个数
给定字符矩阵,单词,判断矩阵里有没有该一条路径组成该单词
template
class priqueue{
public:
priqueue(int m){//构造函数传入队列的总长度
maxsize=m;
x=new T[maxsize+1];//这里为了计数方便,让下标从1开始
//此时,如果一个元素下标为i,则它的
//左子节点在数组中的下标就是2i
//右子节点在数组中的下标是2i+1
n=0;
}
void Add(T t){//这里构建一个小顶堆
x[++n]=t;
int p;
for(int i=n;i>1 && x[p=i/2]>x[i];i=p){
swap(p,i);
}
}
T extractMin(){
T temp=x[1];//取出堆顶权值最大的那个元素
x[1]=x[n--];//序列长度减1,并将最后一个元素放在堆顶
int p;
for(int i=1;(p=2i)x[p+1])
p++;
if(x[p]>=x[i])//移动到位
break;
swap(p,i);
}
return temp;
}
private:
int n;//队列中元素个数
int maxsize;//整个队列的总长度
T *x;//队列指针
void swap(int i,int j){
T temp=x[i];
x[i]=x[j];
x[j]=temp;
}
}
// C++版本实现
class LRUCache {
list> l;
unordered_map>::iterator> mp;
int cap;
public:
LRUCache(int capacity) {
cap = capacity;
}
int get(int key) {
if(mp.find(key) != mp.end())
{
int res = (*mp[key]).second;
l.erase(mp[key]);
l.push_front(make_pair(key, res));
mp[key] = l.begin();
return res;
}
return -1;
}
void put(int key, int value) {
if(mp.find(key) != mp.end())
{
l.erase(mp[key]);
l.push_front(make_pair(key, value));
mp[key] = l.begin();
}
else
{
if(l.size() == cap)
{
mp.erase(l.back().first);
l.pop_back();
}
l.push_front(make_pair(key, value));
mp[key] = l.begin();
}
}
};
/**
* Your LRUCache object will be instantiated and called as such:
* LRUCache* obj = new LRUCache(capacity);
* int param_1 = obj->get(key);
* obj->put(key,value);
*/
// 这也是逐个遍历交换节点的val,但是不涉及指针的改变
list_node * selection_sort(list_node * head,int n){
//在下面完成代码
list_node* cur = head;
list_node* ans = cur;
while(cur != nullptr){
int Min = cur->val; // 假设当前值是最小值
list_node* m = cur;
head = cur->next;
while(head != nullptr){
if(head->val < Min){ // 如果是降序排列就直接把这里的<改成>
m = head;
Min = head->val;
}
head = head->next;
}
swap(cur->val,m->val);
cur = cur->next;
}
return ans;
}
#include
using namespace std;
struct ListNode{
int m_nKey;
ListNode* m_pNext;
};
ListNode* ReverseList(ListNode *pHead){
if(pHead==nullptr){
return nullptr;
}
ListNode* pReversedHead = nullptr;
ListNode* pNode = pHead;
ListNode* pPrev = nullptr;
while(pNode != nullptr){
ListNode* pNext = pNode->m_pNext;
if(pNext==nullptr){
pReversedHead = pNode;
}
pNode->m_pNext = pPrev;
pPrev = pNode;
pNode = pNext;
}
return pReversedHead;
}
class Solution {
public:
int reverse(int x) {
int rev = 0;
while (x != 0) {
int pop = x % 10;
x /= 10;
if (rev > INT_MAX/10 || (rev == INT_MAX / 10 && pop > 7))
return 0;
if (rev < INT_MIN/10 || (rev == INT_MIN / 10 && pop < -8))
return 0;
rev = rev * 10 + pop;
}
return rev;
}
};
ListNode* MergeRepeat(ListNode* pHead1, ListNode* pHead2){
if(l1==NULL) return l2;
if(l2==NULL) return l1;
if(l1->val <= l2->val){
l1->next = MergeRepeat(l1->next, l2);
return l1;
}
else{
l2->next = MergeRepeat(l1, l2->next);
return l2
}
}
// 方法一
class Solution{
public:
// 递归合并两个有序链表
ListNode* merge(ListNode* p1, ListNode* p2){
if(!p1) return p2; // 递归终止条件
if(!p2) return p1;
if(p1->val <= p2->val){
p1->next = merge(p1->next, p2);
return p1; // 最后的返回值
}else{
p2->next = merge(p1, p2->next);
return p2;
}
}
ListNode* mergeKLists(vector& lists) {
if(lists.size() == 0) return nullptr;
ListNode* head = lists[0];
for(int i = 1; i& lists) {
ListNode* pNode = nullptr;
vector elements;
for(int i=0; ival);
pNode = pNode->next;
}
}
sort(elements.begin(), elements.end());
ListNode* res = new ListNode(-1);
ListNode* ptr;
ptr = res;
for(int j=0; jnext = new ListNode(elements[j]);
ptr = ptr->next;
}
return res->next;
}
};
// 利用动态规划来做
#include
using namespace std;
// 做法一
class Solution{
public:
int getMaxSumOfSubSeq(vector &nums){
int len=nums.size();
vector dp(len+1, 0);
dp[0] = nums[0];
int sum=nums[0];
for(int i=1; i=0) sum+=nums[i];
else sum=nums[i];
dp[i] = max(sum, dp[i-1]);
}
return dp[i];
}
};
// 做法二
class Solution{
public:
int maxSubArray(vector &nums){
int pre=0;
int maxAns=nums[0];
for(int i=0; i
// 方法一: 暴力解。可以保证排后数据的相对位置不改变
// 方法二: 设置两个指针,遇到一个奇数和偶数时进行交换,但是这个不能保证数据的相对顺序固定不变。
// 题目就是不同路径的问题
// 利用动态规划,可以是一维的数组,也可以是一个二维矩阵
class Solution {
public:
int uniquePaths(int m, int n) {
vector> dp(m, vector(n, 0));
for(int i = 0; i < m; ++i){
for(int j = 0; j < n; ++j){
dp[i][j] = (i > 0 && j >0 ) ? dp[i][j] = dp[i][j-1] + dp[i-1][j] : 1;
}
}
return dp[m-1][n-1];
}
};
// 拓展到存在障碍的路径问题
class Solution {
public:
int uniquePathsWithObstacles(vector>& obstacleGrid) {
int m = obstacleGrid.size();
if (m < 1) return 0;
int n = obstacleGrid[0].size();
if (n < 1) return 0;
long dp[m][n]; // 使用int提交出现溢出错误,就改为long
if (1 == obstacleGrid[0][0]) return 0;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (0 == i && 0 == j) { //上面判断过(0,0)为1的情况了,这里必定是没有障碍物,因此路径为1
dp[i][j] = 1;
} else if (0 == i && j != 0) { // 最上面一行网格,如果该点是障碍物,则这一点必定不可达,否则路径和达到其左侧的路径一样
dp[i][j] = (1 == obstacleGrid[i][j] ? 0 : dp[i][j - 1]);
} else if (0 != i && j == 0) { // 最左侧一列网格,如果该点是障碍物,则这一点必定不可达,否则路径和达到其上侧的路径一样
dp[i][j] = (1 == obstacleGrid[i][j] ? 0 : dp[i - 1][j]);
} else { // 对于坐标均不为0的点,仅在该点为障碍物的时候不可达
dp[i][j] = (1 == obstacleGrid[i][j] ? 0 : dp[i][j - 1] + dp[i - 1][j]);
}
}
}
return static_cast(dp[m - 1][n - 1]);
}
};
class Solution {
public:
int minNumberOfFrogs(string croakOfFrogs) {
int c=0;
int r=0;
int o=0;
int a=0;
int k=0;
int re=0;
bool flag=true;
for(int i=0; i=r && r>=o && o>=a && a>=k){
c--;
r--;
o--;
a--;
k--;
}
}
if(!(c>=r && r>=o && o>=a && a>=k)){//必须保持任意时刻(c>=r>=o>=a>=k),才是正确的;否则就是错误的,
flag=false;
break;
}
}
if (c!=0 || r!=0 || o!=0 || a!=0 ||k!=0) flag=false;//如果最后有剩的字母,也是错误的
if (flag==true) return re;
else return -1;
}
};
//直接输入两颗树判断左右子树是否相等
class Solution {
public:
bool isSymmetric(TreeNode* root) {
if (root == NULL) return true;
return isSymmetric(root, root);
}
bool isSymmetric(TreeNode* root1, TreeNode* root2){
if(root1==NULL && root2==NULL){
return true;
}
else if(root1==NULL || root2==NULL){
return false;
}
else if(root1->val != root2->val){
return false;
}
return isSymmetric(root1->left, root2->right) && isSymmetric(root1->right, root2->left);
}
};
int longestValidParentheses(string s) {
stack left;//position of '('
for(int ii = 0; ii < s.size(); ++ii){
if (s[ii] == '(') left.push(ii);
else if (!left.empty()){ //')'
s[ii] = 'k';
s[left.top()] = 'k';
left.pop();
}
}
int maxLength = 0;
int length = 0;
for(int ii = 0; ii < s.size(); ++ii){
if (s[ii]=='k'){
++length;
if (maxLength < length) maxLength = length;
}
else length = 0;
}
return maxLength;
}
};
// 1->1->1->2->3 输出:2->3
/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) : val(x), next(NULL) { }
};
*/
class Solution {
public:
ListNode* deleteDuplication(ListNode* pHead)
{
// 链表有0个/1个节点,返回第一个节点
if(pHead==NULL || pHead->next==NULL)
return pHead;
else
{
// 新建一个头节点,防止第一个结点被删除
ListNode* newHead=new ListNode(-1);
newHead->next=pHead;
// 建立索引指针
ListNode* p=pHead; // 当前节点
ListNode* pre=newHead; // 当前节点的前序节点
ListNode* next=p->next; // 当前节点的后序节点
// 从头到尾遍历编标
while(p!=NULL && p->next!=NULL)
{
if(p->val==next->val)//如果当前节点的值和下一个节点的值相等
{
// 循环查找,找到与当前节点不同的节点
while(next!=NULL && next->val==p->val)
{
ListNode* temp=next;
next=next->next;
// 删除内存中的重复节点
delete temp;
temp = nullptr;
}
pre->next=next;
p=next;
}
else//如果当前节点和下一个节点值不等,则向后移动一位
{
pre=p;
p=p->next;
}
next=p->next;
}
return newHead->next;//返回头结点的下一个节点
}
}
};
class Solution{
public:
static constexpr int dirs[4][2] = {
{1,0}, {-1, 0}, {0, 1}, {0, -1}};
int longestIncreasingPath(vector< vector > &matrix){
if (matrix.size() == 0 || matrix[0].size() == 0) {
return 0;
}
int m=matrix.size();
int n=matrix[0].size();
auto mem = vector> (m, vector(n,0));
int res=0;
for(int i=0; i > &matrix, vector> &mem){
if(mem[i][j] != 0){
return mem[i][j];
}
++mem[i][j];
for(int dir=0; dir<4; dir++){
int newi= i+dirs[dir][0];
int newj= j+dirs[dir][1];
if(newi>=0 && newi=0 && newj matrix[i][j]){
mem[i][j]=max(mem[i][j], dfs(newi, newj, m, n, matrix, mem)+1);
}
}
return mem[i][j];
}
};
int longestPalindromeSubseq(string s) {
int n = s.size();
// dp 数组全部初始化为 0
vector> dp(n, vector(n, 0));
// base case
for (int i = 0; i < n; i++)
dp[i][i] = 1;
// 反着遍历保证正确的状态转移
for (int i = n - 1; i >= 0; i--) {
for (int j = i + 1; j < n; j++) {
// 状态转移方程
if (s[i] == s[j])
dp[i][j] = dp[i + 1][j - 1] + 2;
else
dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
}
}
// 整个 s 的最长回文子串长度
return dp[0][n - 1];
}
/*
题目描述:给定一个字符矩阵,判断某个路径是否能组成某个字符串
*/
class Solution{
public:
static constexpr int dirs[4][2] = {
{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
int rows;
int columns;
bool exist(vector>& board, string word){
rows = board.size();
columns = board[0].size();
if(rows==1 && columns==1 && word.size()==1 ){
if(word[0] == board[0][0]){
return true;
}
else{
return false;
}
}
// 遍历每个起点
for(int i=0; i>& board, string word, int path, int row, int col){
if(path == word.size()){
return true;
}
else if(row>=0 && col>=0 && row=0 && newCol>=0 && newRow>& board, string word) {
bool m=false;
for(int i=0;i>& board,string&word ,int row,int col,int Pos)
{
if (row >= board.size() || row < 0 || col >= board[0].size() || col < 0 || board[row][col]!=word[Pos]) return false;
if (Pos == word.size() - 1) return true;
char tmp = board[row][col];
board[row][col] = '#';
bool m=false;
m = dfs(board, word, row + 1, col, Pos + 1) || dfs(board, word, row - 1, col, Pos + 1) || dfs(board, word, row, col + 1, Pos + 1)||dfs(board, word, row, col - 1, Pos + 1);
board[row][col] = tmp;
return m;
}
class Solution {
public:
int movingCount(int m, int n, int k) {
if(k == 0) return 1;
vector > valid(m, vector(n, true)); // 记录该位置是否被访问过
return dfs(valid, m, n, 0, 0, k);
}
int dfs(vector >& valid, int m, int n, int row, int col, int k) // valid的传值一定要用 & !!!
{
int sum = getSum(row) + getSum(col);
// 如果越界,或者和大于k,或者已被访问过了,返回0
if(row>=m || col>=n || sum>k || !valid[row][col]) return 0;
valid[row][col] = false; // 该位置状态变为:已访问过
return 1 + dfs(valid,m,n,row+1,col,k) + dfs(valid,m,n,row,col+1,k); // 回溯法(递归)
}
int getSum(int num)
{
// 求某个数字所有位数相加的和
if(num < 10) return num;
int sum = 0;
while(num > 0)
{
sum += num % 10;
num /= 10;
}
return sum;
}
};
/*
请实现一个函数用来匹配包含'. '和'*'的正则表达式。模式中的字符'.'表示任意一个字符,而'*'表示它前面的字符可以出现任意次(含0次)。在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串"aaa"与模式"a.a"和"ab*ac*a"匹配,但与"aa.a"和"ab*a"均不匹配。
*/
bool isMatch(std::string s, std::string p) {
if (p.empty()) {
return s.empty();
}
// p,s第一个字符是否匹配,相等或为 '.'
bool first_match = (!s.empty() && (p[0] == s[0] || p[0] == '.'));
// 从p的第2个字符开始,如果为 '*'
if (p.size() >= 2 && p[1] == '*') {
// 考虑 '*' 表示前面字符0次和多次的情况
// 0次:s回溯与p第3个字符继续下一轮匹配
// 多次 : 第1个字符匹配,从s第2个字符与p继续下一轮匹配
return (isMatch(s, p.substr(2)) || (first_match && isMatch(s.substr(1), p)));
} else {
//未匹配到 '*',继续普通匹配
return first_match && isMatch(s.substr(1), p.substr(1));
}
}
class Solution {
private:
int cnt=0; // 记录总的逆序对数
public:
void mergesort(int lo,int hi,vector& nums,vector& tmp){
if(lo>=hi) return;
int mid=lo+(hi-lo)/2;
//cout << "mid: " << mid << endl;
mergesort(lo,mid,nums,tmp); // 一直二分
mergesort(mid+1,hi,nums,tmp);
//cout << "mid1: " << mid << endl;
int i=lo,j=mid+1;
//cout << "i: " << i << " " << "j: " << j << endl;
for(int k=lo;k<=hi;k++){
if(i>mid) tmp[k]=nums[j++];//nums[i]到nums[mid]已经全部填入tmp
else if(j>hi) tmp[k]=nums[i++];//nums[mid+1]到nums[j]已经全部填入tmp
else if(nums[i]>nums[j]) {
tmp[k]=nums[j++];
cnt+=mid-i+1;//i肯定小于j,且nums[i]到nums[mid]是升序排序,如果nums[i]>nums[j],说明从nums[i]到nums[mid]和nums[j]都是逆序对
}
else tmp[k]=nums[i++];
}
for(int m=lo;m<=hi;m++) nums[m]=tmp[m];//
}
int reversePairs(vector& nums) {
vector tmp(nums.size(),0);//就是用来记录某个递归函数merge后的情况,然后复制更新nums
mergesort(0,nums.size()-1,nums,tmp);
return cnt;
}
};
class Solution{
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q){
if (root == nullptr){
return nullptr;
}
vector v1; // 保存p的路径
vector v2; // 保存q的路径
TreeNode* res = nullptr;
bool flag1 = findPath(root, p, v1);
bool flag2 = findPath(root, q, v2);
cout << flag1 << " " << flag2 << endl;
if(flag1 == true && flag2 == true){
int i=0, j=0;
while(i &path){
bool flag = false;
path.push_back(root);
if(root == target){
return true;
}
if(root->left != nullptr && flag == false){
flag = findPath(root->left, target, path);
}
if(root->right != nullptr && flag == false){
flag = findPath(root->right, target, path);
}
if(flag == false){
path.pop_back();
}
return flag;
}
};
void Merge(int arr[],int low,int mid,int high);
void MergeSort (int arr [], int low,int high) {
if(low>=high) { return; } // 终止递归的条件,子序列长度为1
int mid = low + (high - low)/2; // 取得序列中间的元素
MergeSort(arr,low,mid); // 对左半边递归
MergeSort(arr,mid+1,high); // 对右半边递归
Merge(arr,low,mid,high); // 合并
}
void Merge(int arr[],int low,int mid,int high){
//low为第1有序区的第1个元素,i指向第1个元素, mid为第1有序区的最后1个元素
int i=low,j=mid+1,k=0; //mid+1为第2有序区第1个元素,j指向第1个元素
int *temp=new int[high-low+1]; //temp数组暂存合并的有序序列
while(i<=mid&&j<=high){
if(arr[i]<=arr[j]) //较小的先存入temp中
temp[k++]=arr[i++];
else
temp[k++]=arr[j++];
}
while(i<=mid)//若比较完之后,第一个有序区仍有剩余,则直接复制到t数组中
temp[k++]=arr[i++];
while(j<=high)//同上
temp[k++]=arr[j++];
for(i=low,k=0;i<=high;i++,k++)//将排好序的存回arr中low到high这区间
arr[i]=temp[k];
delete []temp;//释放内存,由于指向的是数组,必须用delete []
}
class Solution{
public:
vector shellSort(vector nums){
int length = nums.size();
int i, j, gap;
for (gap = length / 2; gap > 0; gap /= 2) // 每次的增量,递减趋势
{
for (i = gap; i < length; i++) //每次增量下,进行几组插入排序,如第一步就是(从12,9,73,26,37)5次
for (j = i ; j -gap >= 0 && nums[j-gap] > nums[j]; j -= gap)// 每个元素组中进行直接插入排序,看例子
swap(nums[j-gap], nums[j]); //如果增量为2时他的插入查询操作下标为:
//(2-0,3-1/ 4-2-0,5-3-1/ 6-4-2-0,7-5-3-1/ 8-6-4-2-0,9-7-5-3-1)
for(int k=0; k
class SolutionRepeat{
public:
void quikSortMain(vector &datas){
int start = 0;
int end = datas.size()-1;
quickSort(datas, start, end);
}
void quickSort(vector &datas, int start, int end){
if (start < end)
{
int splitIndex = getSplitIndex(datas, start, end);
quickSort(datas, start, splitIndex-1);
quickSort(datas, splitIndex+1, end);
}
}
int getSplitIndex(vector &datas, int start, int end){
int randomNum = datas[start];
int left = start+1;
int right = end;
while (left <= right)
{
while (datas[left] <= randomNum && left <= right) left++;
while (datas[right] >= randomNum && left <= right) right--;
if(right < left){
break;
}
else{
int temp = datas[left];
datas[left] = datas[right];
datas[right] = temp;
}
}
int temp = datas[start];
datas[start] = datas[right];
datas[right] = temp;
return right;
}
};
#include
#include
#include
using namespace std;
void HeapAdjust (int data[], int length, int k)
{
int tmp = data[k];
int i=2*k+1;
while (idata[i+1]) //选取最小的结点位置
++i;
if (tmp < data[i]) //不用交换
break;
data[k] = data[i]; //交换值
k = i; //继续查找
i = 2*k+1;
}
data[k] = tmp;
}
void HeapSort (int data[], int length)
{
if (data == NULL || length <= 0)
return;
for (int i=length/2-1; i>=0; --i) {
HeapAdjust(data, length, i); //从第二层开始建堆
}
for (int i=length-1; i>=0; --i) {
std::swap(data[0], data[i]);
HeapAdjust(data, i, 0); //从顶点开始建堆, 忽略最后一个
}
return;
}
int main (void)
{
int data[] = {49, 38, 65, 97, 76, 13, 27, 49};
int length = 8;
HeapSort(data, length);
for (int i=0; i
- 海量文本的处理问题