0%

machine translation 这个任务一般是作为language modeling的紧接一个话题。它的前身(2010年之前)是statistical machine translation,但自从Neural machine translation出来之后,用statistical的方式来做translation就少了很多。有兴趣的可以了解下statistical machine translation的具体细节. 本博客主要记录NMT的主要论文和研究。NMT的架构主要是encoder-decoder架构,它其实是一个很典型的seq-to-seq的模型, 关于它的定义:

Neural Machine Translation (NMT) is a way to do Machine Translation with a single end-to-end neural network

它的一般架构是这样的:

Seq2Seq

NMT所有的模型都基于一个统一的数学公式:

数学公式

注意这里和statistical machine translation的公式是不一样的:

statistical machine translation

用统计翻译模型做的时候是分别解决translation model以及language model的问题,涉及很多特征工程的问题,很复杂。

在machine translation领域,encoder-decoder架构的模型经历了好几次演变,最终才转化成加入了attention机制,模型架构的整理可以参考Neural Machine Translation: A Review and Survey。文章的第五章介绍了将encoder编码为固定长度的向量的用法。其中有两种使用这个C的用法,1. 作为decoder的初始化state 2. 作为decoder每一个时间步的固定输入和input一起去计算hidden state:

Encoder-decoder architectures with fixed-length sentence encodings

这些文章从Sequence to Sequence Learning with Neural Networks,再到Learning Phrase Representations using RNN Encoder-Decoder for Statistical Machine Translation. 然后就过度到attention时代了,所以作者在这篇review中只花了很少的第五章节就结束了。第六章就开始讲attentional encoder-decoder networks。

The concept of attention is no longer just a technique to improve sentence lengths in NMT. Since its introduction by Bahdanau et al. (2015) it has become a vital part of various NMT architectures, culminating in the Transformer architecture

这句话是6.1的精髓,attention的概念不再是我们上文所说的那些用于初始化呀,还是用作duplicate context。Bahdanau 2015年的这篇文章,也就是引入multi-head attention的这篇文章彻底打破了这个convention。因为我们可以看到transformer的架构中都没有RNN的身影,有的只是attention weights的计算。

Learning Phrase Representations using RNN Encoder-Decoder for Statistical Machine Translation 2014

这是在机器翻译领域encoder-decoder架构,在attention 机制提出之前表现最好的RNN模型。其实模型挺简单的,encoder负责将input sequence编码成了一个固定的向量Context,然后基于这个向量,decoder每一个时间步产生一个单词。在decoder的每一个时间步进行的运算是:

image-20221212164533545 y_t是由s_t得到的。

同样的,这篇文章可以结合代码来看,轻易理解。该代码是用pytorch实现的。这个pytorch的实现是从Sequence to Sequence Learning with Neural Networks开始讲解的,Learning Phrase Representations using RNN Encoder-Decoder for Statistical Machine Translation这篇文章进步在

image-20221216102034819

可以看到该篇文章介绍的模型优势在于预测y的时候加入了context以及\(y_{t-1}\),而不是仅仅依赖于\(s_t\)

以上的文章都是将input sentence编码成一个fixed-length的vector,从下面这篇2015年Bahdanau的文章开始,attention就开始用于NMT。为了解决fixed-length vector的问题,这样我们就不必要将input sentence的所有信息都编码到一个固定长度的向量里。

Neural Machine Translation by Jointly Learning to Align and Translate 2015

从这篇文章开始,attention的机制开始使用在翻译中。

在Introduction章节,最重要的一句话:

The most important distinguishing feature of this approach from the basic encoder–decoder is that it does not attempt to encode a whole input sentence into a single fixed-length vector. Instead, it encodes the input sentence into a sequence of vectors and chooses a subset of these vectors adaptively while decoding the translation

意即跟以往那种encoder-decoder的网络来做translation的model不同,虽然提出的模型也属于encoder-decoder架构,但不是将input sentence编码成一个固定长度的向量,而是将input sentence编码成一系列的向量并自适应的从中选择一个小子集的向量用来做decode。

截至文章发表,现有做机器翻译的模型中,表现最好的模型是RNN,内units用lstm。可以称之为RNN Encoder-Decoder。

还有一个发现是,这些encoder和decoder block,里面基本上是stacked rnns结构,也就是堆了好几层rnn。这个发现可以追溯到paper. 该作者发现在NMT任务上,high-performing rnns are usually multi-layer, 不仅如此,对于encoder rnn,2到4层是最好的,对于decoder rnn,4层是最好的。通常情况下,2层堆叠的RNN比一层RNN要lot better; 为了解决long dependency的问题,用lstm cell是必要的,但这也不够,需要使用一些其他的技术,比如skip-connection,dense-connections。


这里值得一提的是,虽然Bahdanau 2015年出的这篇文章很火。但是后来通过学习cs224n和观察tensorflow的文档:Neural machine translation with attention,发现luong 2015的这篇文章中的架构使用的更多,它的计算公式和Bahdanau介绍的有一点点不一样,再luong的文章中我们也可以看到它自己说的和Bahdanau不一样的地方:

image-20230313171420621

Comparison to (Bahdanau et al., 2015) – While our global attention approach is similar in spirit to the model proposed by Bahdanau et al. (2015), there are several key differences which reflect how we have both simplified and generalized from the original model. First, we simply use hidden states at the top LSTM layers in both the encoder and decoder as illustrated in Figure 2. Bahdanau et al. (2015), on the other hand, use the concatenation of the forward and backward source hidden states in the bi-directional encoder and target hidden states in their non-stacking unidirectional decoder. Second, our computation path is simpler; we go from ht → at → ct → ̃ ht then make a prediction as detailed in Eq. (5), Eq. (6), and Figure 2. On the other hand, at any time t, Bahdanau et al. (2015) build from the previous hidden state ht−1 → at → ct → ht, which, in turn, goes through a deep-output and a maxout layer before making predictions.7 Lastly, Bahdanau et al. (2015) only experimented with one alignment function, the concat product; whereas we show later that the other alternatives are better.

所以关于用attention来做machine translation的模型,我们只需要记住下面的计算过程就行,因为它也不是现在流行的machine translation的方法(毕竟2015年的时候transformer还没出来):

attention in equations

以上的模型给我们解决了标准的seq2seq的模型在做NMT任务时的一些问题:

  • improves NMT performance
  • provides more "human-like" model: replace the fixed length vector with dynamic vector according to the decoder hidden states
  • solves the bottleneck problem: allows decoder to look directly at source
  • helps with the vanishing gradient problem
  • provides some interpretability

注意,虽然attention机制首先是在NMT任务中提出并得到了应用,但是它并不是seq2seq的专属,你也可以将attention用在很多architectures和不同的tasks中。有一个关于attention的更general的定义是:

general definition of attention

我们有时候会说: query attends to the values,例如在seq2seq2+attention的模型中,每一个decoder hidden state就是query,attends to 所有的encoder hidden states(values).

Attention is all you need 2017

在transformer的paper中,作者首先介绍本文:主流的sequence tranduction模型主要基于复杂的RNN或者CNN模型,它们包含encoder和decoder两部分,其中表现最好的模型在encoder和decoder之间增加了attention mechanism。本文提出了一个新的简单的网络结构名叫transformer,也是完全基于attention机制,"dispensing with recurrence and convolutions entirely"! 根本无需循环和卷积!了不起的Network~

在阅读这篇文章之前需要提前了解我在另外一篇博客 Attention and transformer model中的知识,在translation领域我们的科学家们是如何从RNN循环神经网络过渡到CNN,然后最终是transformer的天下的状态。技术经过了一轮轮的迭代,每一种基础模型架构提出后,会不断的有文章提出新的改进,文章千千万,不可能全部读完,就精读一些经典文章就好,Vaswani这篇文章是NMT领域必读paper,文章不长,加上参考文献才12页,介绍部分非常简单,导致这篇文章的入门门槛很高(个人感觉)。我一开始先读的这篇文章,发现啃不下去,又去找了很多资料来看,其中对我非常帮助的有很多:

  • 非常通俗易懂的blog 有中文版本的翻译
  • Neural Machine Translation: A Review and Survey 虽然这篇paper很长,90+页。前六章可以作为参照,不多25页左右,写的非常好
  • stanford cs231n课程的ppt 斯坦福这个课程真的很棒,youtube上可以找到17年的视频,17年的课程中没有attention的内容,所以就姑且看看ppt吧,希望斯坦福有朝一日能将最新的课程分享出来,也算是做贡献了
  • cs231n推荐的阅读博客 非常全面的整理,强烈建议食用. 这位作者也附上了自己的transformer实现,在它参考的那些github实现里,哈佛大学的pytorch实现也值得借鉴。
  • The annotated Transformer 斯坦福出的关于Attention is All you need学术文章的解析以及代码实现,强烈建议食用。

Transformer这篇文章有几个主要的创新点:

  1. 使用self-attention机制,并首次提出使用multi-head attention

该机制作用是在编码当前word的时候,这个self-attention就会告诉我们编码这个词语我们应该放多少注意力在这个句子中其他的词语身上,说白了其实就是计算当前词语和其他词语的关系。这也是CNN用于解决NMT问题时用不同width的kernel来扫input metric的原因。

multi-head的意思是我使用多个不同的self-attention layer来处理我们的输入,直观感觉是训练的参数更多了,模型的表现力自然要好一点。

  1. Positional embeddings

前一个创新点解决了dependence的问题,那如何解决位置的问题呢?也就是我这个词在编码的时候或者解码的时候应该放置在句子的哪个位置上。文章就用pisitional embedding来解决这个问题。这个positional embedding和input embedding拥有相同的shape,所以两者可以直接相加。transformer这篇文章提供了两种encoding方式:

1) sunusoidal positional encoding

image-20230302133247664

其中,pos=1,...,L(L是input句子的长度),i是某一个PE中的一个维度,取值范围是1到dmodel。python实现为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def positional_encoding(length, depth):
depth = depth/2

positions = np.arange(length)[:, np.newaxis] # (seq, 1)
depths = np.arange(depth)[np.newaxis, :]/depth # (1, depth)

angle_rates = 1 / (10000**depths) # (1, depth)
angle_rads = positions * angle_rates # (pos, depth)

pos_encoding = np.concatenate(
[np.sin(angle_rads), np.cos(angle_rads)],
axis=-1)

return tf.cast(pos_encoding, dtype=tf.float32)

pos_encoding = positional_encoding(length=2048, depth=512)

# Check the shape.
print(pos_encoding.shape) # (2014,512)

2) learned positional encoding

整体上看,这篇文章提出的transformer模型在做translation的任务时,架构是这样的:

image-20230301133249379

其中encoders部分包含了6个encoders的block,decoders部分也包含了6个decoders的block,将encoders的每一个block拆开来看,有两个sub layer:

image-20230301133424302
image-20230301133440152

其中decoder部分的block比encoder部分的block多了一个sub layer,其中self-attention和encoder-decoder attention都是multi-head attention layer,只不过decoder部分的第一个multi-head attention layer是一个masked multi-head attention,为了防止未来的信息泄露给当下(prevent positions from attending to the future).

image-20230301133608484

在transformer模型中,作者还使用了residual connection,所以在encoder的每一个block中,数据的flow是:

transformer架构

其中self-attention中涉及的运算details是:

image-20230301134258180

可以发现其中涉及的运算都是矩阵的点乘,并没有RNN中那种时间步的概念,所以所有运算都是可以parallelizable,这就能使得模型的推理和训练更加的efficient。并且!Transformers也可以抓住distant的依赖,而不是像rnn那样对于长依赖并不是很擅长,因为它前面的信息如果像传递到很后面的单词推理上,需要经历很多时间步的计算,而transformer在推理每一个单词的时候都可以access到input句子中的每一个单词(毕竟我们的Z中包含了每一个单词跟其他单词的关系)。

其中positional encoding现在可以简单的理解成在我们编码的word embedding上我们又加了一个positional encoding,维度和我们的embedding一模一样。

在tensorflow中有一个layer是MultiHeadAttention,如果我们想实现transformer里的这个self-attention,那就是query,key,value其实都是由input vector计算来的。

以上的理论计算看起来可能会有点模糊,可以同步参照博客 参考 illustrated transformer介绍的详细细节,基于tensorflow框架实现的transformer来帮助自己理解transformer模型。

encoder部分

encoder的每一个block由两个sub-layer组成,中间穿插resnet connection。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
def multihead_attention(self, query, memory=None, mask=None, scope='attn'):
"""
Args:
query (tf.tensor): of shape (batch, q_size, d_model)
memory (tf.tensor): of shape (batch, m_size, d_model)
mask (tf.tensor): shape (batch, q_size, k_size)

Returns:h
a tensor of shape (bs, q_size, d_model)
"""
if memory is None: # 如果memory是None,那么就是一个典型的self-attention layer
memory = query

with tf.variable_scope(scope):
# Linear project to d_model dimension: [batch, q_size/k_size, d_model]
Q = tf.layers.dense(query, self.d_model, activation=tf.nn.relu)
K = tf.layers.dense(memory, self.d_model, activation=tf.nn.relu)
V = tf.layers.dense(memory, self.d_model, activation=tf.nn.relu)

# Split the matrix to multiple heads and then concatenate to have a larger
# batch size: [h*batch, q_size/k_size, d_model/num_heads]
Q_split = tf.concat(tf.split(Q, self.h, axis=2), axis=0)
K_split = tf.concat(tf.split(K, self.h, axis=2), axis=0)
V_split = tf.concat(tf.split(V, self.h, axis=2), axis=0)
mask_split = tf.tile(mask, [self.h, 1, 1])

# Apply scaled dot product attention
out = self.scaled_dot_product_attention(Q_split, K_split, V_split, mask=mask_split)

# Merge the multi-head back to the original shape
out = tf.concat(tf.split(out, self.h, axis=0), axis=2) # [bs, q_size, d_model]

# The final linear layer and dropout.
# out = tf.layers.dense(out, self.d_model)
# out = tf.layers.dropout(out, rate=self.drop_rate, training=self._is_training)

return out

def feed_forwad(self, inp, scope='ff'):
"""
Position-wise fully connected feed-forward network, applied to each position
separately and identically. It can be implemented as (linear + ReLU + linear) or
(conv1d + ReLU + conv1d).

Args:
inp (tf.tensor): shape [batch, length, d_model]
"""
out = inp
with tf.variable_scope(scope):
# out = tf.layers.dense(out, self.d_ff, activation=tf.nn.relu)
# out = tf.layers.dropout(out, rate=self.drop_rate, training=self._is_training)
# out = tf.layers.dense(out, self.d_model, activation=None)

# by default, use_bias=True
out = tf.layers.conv1d(out, filters=self.d_ff, kernel_size=1, activation=tf.nn.relu)
out = tf.layers.conv1d(out, filters=self.d_model, kernel_size=1)

return out

def encoder_layer(self, inp, input_mask, scope):
"""
Args:
inp: tf.tensor of shape (batch, seq_len, embed_size)
input_mask: tf.tensor of shape (batch, seq_len, seq_len)
"""
out = inp
with tf.variable_scope(scope):
# One multi-head attention + one feed-forword
out = self.layer_norm(out + self.multihead_attention(out, mask=input_mask))
out = self.layer_norm(out + self.feed_forwad(out))
return out

decoder部分

在decoder部分,我们可以看到每一个decoder block的输入有两个:整个encoder部分的输出以及上一个decoder block的输出(第一个decoder block是词向量的输入),而encoder部分的输出是接到每一个decoder block的第二个sublayer的。正如刚刚提到了,decoder部分的每一个block跟encoder部分的block有一个不一样的地方,那就是多了一个sublayer: encoder-decoder attention。至于encoder部分和decoder部分是如何connect的,

The encoder start by processing the input sequence. The output of the top encoder is then transformed into a set of attention vectors K and V. These are to be used by each decoder in its “encoder-decoder attention” layer which helps the decoder focus on appropriate places in the input sequence

也就是我们得到了encoder部分top layer(最后一个encoder layer)的输出之后,我们将输出转化成K和V. 我们可以看到在multihead_attention里,memory是enc_out

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def decoder_layer(self, target, enc_out, input_mask, target_mask, scope):
out = target
with tf.variable_scope(scope):
out = self.layer_norm(out + self.multihead_attention(
out, mask=target_mask, scope='self_attn'))
out = self.layer_norm(out + self.multihead_attention(
out, memory=enc_out, mask=input_mask)) # 将encoder部分的输出结果作为输入
out = self.layer_norm(out + self.feed_forwad(out))
return out

def decoder(self, target, enc_out, input_mask, target_mask, scope='decoder'):
out = target
with tf.variable_scope(scope):
for i in range(self.num_enc_layers):
out = self.decoder_layer(out, enc_out, input_mask, target_mask, f'dec_{i}')
return out

以上实现的transformer其实我觉得还是有一点点复杂,毕竟在tensorflow2.0+版本中已经有了官方实现好的layers.MultiHeadAttention可以使用,应该可以大大简化我们实现步骤,特别是上面的def multihead_attention(self, query, memory=None, mask=None, scope='attn'):。从刚刚的实现里我们可以发现,除了decoder部分每一个block的第二个sublayer的attention计算有一点不一样之外,其他的attention计算都是一模一样的。我在github上找了不少用TF2.0实现的transformer(最标准的也是Attention is all you need的模型),发现很多都都写得一般般,最终发现还是tensorflow官方文档写的tutotial写的最好.

现在对照tensorflow的tutorial以及上面transformer的计算过程,拆解一下官方给的代码。

首先定义一个baseAttention类,然后在此基础上我们再定义encoder和decoder中的attention:

1
2
3
4
5
6
class BaseAttention(tf.keras.layers.Layer):
def __init__(self, **kwargs):
super().__init__()
self.mha = tf.keras.layers.MultiHeadAttention(**kwargs)
self.layernorm = tf.keras.layers.LayerNormalization()
self.add = tf.keras.layers.Add()

那么针对encoder结果输入到decoder的cross attention layer怎么处理呢?这时候我们使用MultiHeadAttention时就需要将target sequence x当作是query,将encoder输出当作是context sequence也就是key/value。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class CrossAttention(BaseAttention): # encoder结果输入到decoder的层
def call(self, x, context): # 这里的x是target sequence,context是encoder的输出结果
attn_output, attn_scores = self.mha(
query=x,
key=context,
value=context,
return_attention_scores=True)

# Cache the attention scores for plotting later.
self.last_attn_scores = attn_scores

x = self.add([x, attn_output])
x = self.layernorm(x)

return x

然后我们再定义global attention,global attention就是没有任何特殊操作的(比如上面的attention计算它有特别的context),而在transformor中更多的是self-attention,也就是我们传递给MultiHeadAttention的query,key,value都是同一个值。

1
2
3
4
5
6
7
8
9
class GlobalSelfAttention(BaseAttention):
def call(self, x):
attn_output = self.mha(
query=x,
value=x,
key=x)
x = self.add([x, attn_output])
x = self.layernorm(x)
return x

最后我们定义causal self attention layer,这个是在decoder的每一个block的第一个sublayer:self-attention layer.其实这个layer是和global attention layer差不多的,但还是有一点微小的差别。为什么呢?因为我们在decoder阶段,我们是一个词语一个词语的预测的,这其实包含了一层因果关系,我们在预测一个词语的时候,我们应该已知它前面一个词语是什么,RNN中的hidden state传递到下一个时间步就是这个因果关系的传递。那么如果我们使用刚刚我们实现的global attention layer来实现这个self attention,并没有包含这个因果关系,不仅如此,如果我们使用常规的self attention的计算,将target sequence全部当作输入输入到decoder中的第一个block中,会有未来的数据提前被当前时刻看到的风险,所以在Transformer这篇文章中,作者提出使用mask的技术来避免这个问题。

在tensorflow中实现很简单,就只需要给MultiHeadAttention传递一个use_causal_mask = True的参数即可:

1
2
3
4
5
6
7
8
9
10
class CausalSelfAttention(BaseAttention):
def call(self, x):
attn_output = self.mha(
query=x,
value=x,
key=x,
use_causal_mask = True) # The causal mask ensures that each location only has access to the locations that come before it
x = self.add([x, attn_output])
x = self.layernorm(x)
return x

这样就可以保证先前的sequence并不依赖于之后的elements。这里我本来有一个疑问是,这样一来这个causal layer并不能实现bi-rnn的能力?但后来一想并不是,因为双向的RNN的后向是指后面的词语先输入,其实就是从后往前输入,这样就可以知道一个sequence当前词语依赖于后面的词语的权重。

补充介绍

tf.keras.layers.MultiHeadAttention

doc

image-20230309150541287
image-20230309150600806

注意,return的结果包含两个,其中attention_output的shape的第二维是和target sequence的长度是一致的,并且E是和query的最后一维是一致的。

Attention Family

这个章节整理于blog,这个作者之前写了一篇介绍attention的文章,后面在2023年一月的时候又更新了两篇博客,详细介绍了从2020年以来出现的新的Transformer models。权当自己学习记录一些我还需要补充的知识。

The Transformer (which will be referred to as “vanilla Transformer” to distinguish it from other enhanced versions; Vaswani, et al., 2017) model has an encoder-decoder architecture, as commonly used in many NMT models. Later simplified Transformer was shown to achieve great performance in language modeling tasks, like in encoder-only BERT or decoder-only GPT.

在上Coursera上关于Tensorflow的高级用法课程时,老师简略介绍了custom layer和custom model的用法,但后来看到其实课程覆盖的内容比较简单,除了介绍了__init__和call两个可override的function外没有介绍其他的。偶然看到一篇博客详细介绍了在tensorflow中如何使用sub classing来搭建模型,写的非常好,这里贴上链接

我们知道在tensorflow中有三种搭建模型的方式: 1) sequential API 也就是想创建一个Sequential实例,然后通过add的方式把一个layer加到模型中去,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# declare input shape 
seq_model = tf.keras.Sequential()
seq_model.add(tf.keras.Input(shape=imput_dim))

# Block 1
seq_model.add(tf.keras.layers.Conv2D(32, 3, strides=2, activation="relu"))
seq_model.add(tf.keras.layers.MaxPooling2D(3))
seq_model.add(tf.keras.layers.BatchNormalization())

# Block 2
seq_model.add(tf.keras.layers.Conv2D(64, 3, activation="relu"))
seq_model.add(tf.keras.layers.BatchNormalization())
seq_model.add(tf.keras.layers.Dropout(0.3))

# Now that we apply global max pooling.
seq_model.add(tf.keras.layers.GlobalMaxPooling2D())

# Finally, we add a classification layer.
seq_model.add(tf.keras.layers.Dense(output_dim))
sequential的方式在researcher中用的不多,随着模型变得越来越复杂,可以看到tensorflow的application模块实现的官方模型代码中,已经见不到这种形式了。 2) Functional API 正如其名,就是用函数调用的方式来搭建模型:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# declare input shape 
input = tf.keras.Input(shape=(imput_dim))

# Block 1
x = tf.keras.layers.Conv2D(32, 3, strides=2, activation="relu")(input)
x = tf.keras.layers.MaxPooling2D(3)(x)
x = tf.keras.layers.BatchNormalization()(x)

# Block 2
x = tf.keras.layers.Conv2D(64, 3, activation="relu")(x)
x = tf.keras.layers.BatchNormalization()(x)
x = tf.keras.layers.Dropout(0.3)(x)

# Now that we apply global max pooling.
gap = tf.keras.layers.GlobalMaxPooling2D()(x)

# Finally, we add a classification layer.
output = tf.keras.layers.Dense(output_dim)(gap)

# bind all
func_model = tf.keras.Model(input, output)
注意:这种方式最终要使用tf.keras.Model()来将inputs和outputs接起来。

  1. Model sub-classing API 第三种方式是现在用的最多的方式。 之前我没理解layer和model两种调用方式的区别,我觉得就是一系列运算,我们把输入输进来,return output结果的一个过程。但如果一个类它是Layer的子类,它比model的子类多了一个功能,它有state属性,也就是我们熟悉的weights。比如Dense layer,我们知道它做了线性运算+激活函数,其中的weights就是我们assign给每一个feature的权重,但其实我们并不只是想要这一类别的运算,比如下面的:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    class SimpleQuadratic(Layer):

    def __init__(self, units=32, activation=None):
    '''Initializes the class and sets up the internal variables'''
    # YOUR CODE HERE
    super(SimpleQuadratic, self).__init__()
    self.units = units
    self.activation = tf.keras.activations.get(activation)

    def build(self, input_shape):
    '''Create the state of the layer (weights)'''
    # a and b should be initialized with random normal, c (or the bias) with zeros.
    # remember to set these as trainable.
    # YOUR CODE HERE
    a_init = tf.random_normal_initializer()
    b_init = tf.random_normal_initializer()
    c_init = tf.zeros_initializer()

    self.a = tf.Variable(name = "kernel", initial_value = a_init(shape= (input_shape[-1], self.units),
    dtype= "float32"), trainable = True)

    self.b = tf.Variable(name = "kernel", initial_value = b_init(shape= (input_shape[-1], self.units),
    dtype= "float32"), trainable = True)

    self.c = tf.Variable(name = "bias", initial_value = c_init(shape= (self.units,),
    dtype= "float32"), trainable = True)

    def call(self, inputs):
    '''Defines the computation from inputs to outputs'''
    # YOUR CODE HERE
    result = tf.matmul(tf.math.square(inputs), self.a) + tf.matmul(inputs, self.b) + self.c
    return self.activation(result)
    上面的代码将inputs平方之后和a做乘积,之后再加上inputs和b的乘积,最终返回的是和。这样的运算是tf.keras.layer中没有的。这个时候我们自己customize layer就很方便。还有一个很方便的地方在于很多模型其实是按模块来的,模块内部的layer很类似。这个时候我们就可以把这些模型内的layer包起来变成一个layer的子类(Module),再定义完这些module之后我们使用Model把这些module再包起来,这就是我们最终的model。这时候我们就可以看到Model和Layer子类的区别了,虽然两者都可以实现输入进来之后实现一系列运算返回运算结果,但后者可以实现更灵活的运算,而前者往往是在把每一个模块定义好之后最终定义我们训练模型的类。 > In general, we use the Layer class to define the inner computation blocks and will use the Model class to define the outer model, practically the object that we will train. ---粘贴自博客

You can treat any model as if it were a layer by invoking it on an Input or on the output of another layer. By calling a model you aren't just reusing the architecture of the model, you're also reusing its weights

同样值得注意的是,model的子类也可以像layer那样使用functional API来调用,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
encoder_input = keras.Input(shape=(28, 28, 1), name="original_img")
x = layers.Conv2D(16, 3, activation="relu")(encoder_input)
x = layers.Conv2D(32, 3, activation="relu")(x)
x = layers.MaxPooling2D(3)(x)
x = layers.Conv2D(32, 3, activation="relu")(x)
x = layers.Conv2D(16, 3, activation="relu")(x)
encoder_output = layers.GlobalMaxPooling2D()(x)

encoder = keras.Model(encoder_input, encoder_output, name="encoder")
encoder.summary()

decoder_input = keras.Input(shape=(16,), name="encoded_img")
x = layers.Reshape((4, 4, 1))(decoder_input)
x = layers.Conv2DTranspose(16, 3, activation="relu")(x)
x = layers.Conv2DTranspose(32, 3, activation="relu")(x)
x = layers.UpSampling2D(3)(x)
x = layers.Conv2DTranspose(16, 3, activation="relu")(x)
decoder_output = layers.Conv2DTranspose(1, 3, activation="relu")(x)

decoder = keras.Model(decoder_input, decoder_output, name="decoder")
decoder.summary()

autoencoder_input = keras.Input(shape=(28, 28, 1), name="img")
encoded_img = encoder(autoencoder_input)
decoded_img = decoder(encoded_img)
autoencoder = keras.Model(autoencoder_input, decoded_img, name="autoencoder")
autoencoder.summary()

我们以sub-classing的方式定义的model是没有办法调用summary来看模型架构的,作者也给出了解决方案:github comments

方法就是在Model的子类中添加build_graph方法:

1
2
3
def build_graph(self, raw_shape):
x = tf.keras.layers.Input(shape=raw_shape)
return Model(inputs=[x], outputs=self.call(x))
这样我们就可以正常调用summary()
1
2
3
4
5
6
7
8
cm.build_graph(raw_input).summary()
# 不仅如此还能使用tf.keras.utils.plot_model来生成png
tf.keras.utils.plot_model(
model.build_graph(raw_input), # here is the trick (for now)
to_file='model.png', dpi=96, # saving
show_shapes=True, show_layer_names=True, # show shapes and layer name
expand_nested=False # will show nested block
)

作者同样推荐了一篇博客讲tensorflow中保存模型的各种方式:博客地址.非常推荐阅读

总结一下就是:

  1. 对于Functional API创建的模型,最好的保存模型和导入模型的方式是:
1
2
3
model.save('path_to_my_model.h5')
del model
model = keras.models.load_model('path_to_my_model.h5')

以上方式会将模型的架构,weights以及训练过程中的设定(也就是model.compile())的内容全部保存。

  1. 对于sub class创建的模型,推荐的方式是用save_weights
1
model.save_weights('path_to_my_weights', save_format='tf')

如果想要加载weights,必须要知道原来用sub class建立模型的code。不仅如此,还需要用原来的code先build起模型,让模型知道输入tensor的shape以及dtype,如果没有build这一步程序将会报错。

1
2
3
new_model = MiniInception()
new_model.build((None, x_train.shape[1:])) # or .build((x_train.shape))
new_model.load_weights('net.h5')

tf.function

在我们定义custum training 过程中时我们经常会用到这个装饰器@tf.function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@tf.function
def train_step(step, x, y):
'''
input: x, y <- typically batches
input: step <- batch step
return: loss value
'''
# start the scope of gradient
with tf.GradientTape() as tape:
logits = model(x, training=True) # forward pass
train_loss_value = loss_fn(y, logits) # compute loss

# compute gradient
grads = tape.gradient(train_loss_value, model.trainable_weights)

# update weights
optimizer.apply_gradients(zip(grads, model.trainable_weights))

# update metrics
train_acc_metric.update_state(y, logits)

# write training loss and accuracy to the tensorboard
with train_writer.as_default():
tf.summary.scalar('loss', train_loss_value, step=step)
tf.summary.scalar(
'accuracy', train_acc_metric.result(), step=step
)
return train_loss_value

先看如果一个函数不加这个装饰器会如何:

1
2
3
4
5
6
7
def f(x):
print("Traced with", x)

for i in range(5):
f(2)

f(3)

输出为:

1
2
3
4
5
6
Traced with 2
Traced with 2
Traced with 2
Traced with 2
Traced with 2
Traced with 3

加上装饰器:

1
2
3
4
5
6
7
8
@tf.function
def f(x):
print("Traced with", x)

for i in range(5):
f(2)

f(3)

输出为:

1
2
Traced with 2
Traced with 3

可以看到第二种加了装饰器的方式,即便是循环了5遍,我们仍然只有一行打印了2.

如果我们在上面的代码中print之前加上一行:

1
2
3
4
5
6
7
8
9
@tf.function
def f(x):
print("Traced with", x)
# add tf.print
tf.print("Executed with", x)
for i in range(5):
f(2)

f(3)

程序的输出就变成了:

1
2
3
4
5
6
7
8
Traced with 2
Executed with 2
Executed with 2
Executed with 2
Executed with 2
Executed with 2
Traced with 3
Executed with 3

可以看到tf.print就可以正常按loop运行。注意一点: 被tf.function装饰的函数只能包含operations而不能定义variable比如tf.Variable()

本博客旨在记录自己在了解image classification这个术语computer vision的一个子任务中常见的模型。耳熟能详的就是ResNet,VGG,Inception,MobileNet, Efficientnet。每一个模型之间有什么区别,他们自身又有哪些变种,比如VGG,它拥有VGG16,VGG19等,ResNet又有很多,单是查看Tensorflow的官方文档就会发现在tf.keras.applications模块下,就有很多模型架构可选(也都有预训练参数)。整理这个博客的目的在于让自己对这些模型之间的差别有所了解,这样在不同的任务中才会知道使用什么样的模型架构来handle自己的数据。

在整理这篇博客的过程中,我也去搜了有没有image classification这个单任务上的review文章,文章都挺多的,筛选之后推荐这篇Review of Image Classification Algorithms Based on Convolutional Neural Networks.这篇文章主要是介绍基于CNN的一些模型,共有三个章节。重点是第二章节梳理了CNN-based的一些模型,包括本文想要coverVGGinceptionresnetmobilenet。重点关注图像分类算法的小伙伴可以通读一下这篇文章。

Review of Image Classification Algorithms Based on Convolutional Neural Networks第二章节目录

VGG

首次提出在2014年的paper

img

上图中包含13个卷积层和3个全连接层,是VGG16的结构。而VGG19包含了16个卷积层和3个全连接层:

img

VGG系列就是VGG16VGG19,两者的区别在于19用了更多的卷积层。Tensorflow也提供了这两个模型的黑盒子实现供大家使用。

ResNet

首次提出在2016年的paper,其中最重要的就是网络中的residual block:

img

在作者的原文中我们可以发现,文章中提出的Resnet是34层,也就是ResNet34。在具体实现的时候,作者在每一个卷积操作之后(激活函数之前)加上了batch Normalization。在Module: tf.keras.applications.resnetTensorflow applications resnet中现在只有ResNet101,152,50三个版本,其中ResNet50和ResNet34的区别在于:前者使用三个卷积一个block,后者是2个卷积一个block. ResNet50表现更优异。ResNet101和ResNet152在一个block内使用了更多的卷积layer。

以上所说的都是resnet v1,后来同一个作者又发表了Identity Mappings in Deep Residual Networks,提出了ResNet v2。同样我们在tensorflow中也可以看到模块Module: tf.keras.applications.resnet_v2,同样的也有50,101,152三个版本的model。

v1和v2的区别在于:

ResNet v1 and ResNet v2

以上只是概念上的解释,看代码会更合适一点,其中Deep Residual Learning for Image Recognition文章中也给出了34,50,101,152等几个模型在实现中注意的细节:

image-20230206153053177

ResNet34 V1中,一个resnet block是由两个卷积layer组成的,同时它和V2的一个区别就在于X进来后就先进行卷积运算,也就是上图中的weight

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
import tensorflow as tf
from tensorflow import keras
from keras.activations import relu
from tensorflow.keras.layers import *
from tensorflow.keras import Model
from tensorflow.keras import layers as Layers

class ResBlock(Model):
def __init__(self, channels, stride=1):
super(ResBlock, self).__init__(name='ResBlock')
self.flag = (stride != 1)
self.conv1 = Conv2D(channels, 3, stride, padding='same')
self.bn1 = BatchNormalization()
self.conv2 = Conv2D(channels, 3, padding='same')
self.bn2 = BatchNormalization()
self.relu = ReLU()
if self.flag:
self.bn3 = BatchNormalization()
self.conv3 = Conv2D(channels, 1, stride)

def call(self, x):
x1 = self.conv1(x)
x1 = self.bn1(x1)
x1 = self.relu(x1)
x1 = self.conv2(x1)
x1 = self.bn2(x1)
if self.flag:
x = self.conv3(x)
x = self.bn3(x)
x1 = Layers.add([x, x1])
x1 = self.relu(x1)
return x1


class ResNet34(Model):
def __init__(self):
super(ResNet34, self).__init__(name='ResNet34')
self.conv1 = Conv2D(64, 7, 2, padding='same')
self.bn = BatchNormalization()
self.relu = ReLU()
self.mp1 = MaxPooling2D(3, 2)

self.conv2_1 = ResBlock(64)
self.conv2_2 = ResBlock(64)
self.conv2_3 = ResBlock(64)

self.conv3_1 = ResBlock(128, 2)
self.conv3_2 = ResBlock(128)
self.conv3_3 = ResBlock(128)
self.conv3_4 = ResBlock(128)

self.conv4_1 = ResBlock(256, 2)
self.conv4_2 = ResBlock(256)
self.conv4_3 = ResBlock(256)
self.conv4_4 = ResBlock(256)
self.conv4_5 = ResBlock(256)
self.conv4_6 = ResBlock(256)

self.conv5_1 = ResBlock(512, 2)
self.conv5_2 = ResBlock(512)
self.conv5_3 = ResBlock(512)

self.pool = GlobalAveragePooling2D()
self.fc1 = Dense(512, activation='relu')
self.dp1 = Dropout(0.5)
self.fc2 = Dense(512, activation='relu')
self.dp2 = Dropout(0.5)
self.fc3 = Dense(64)

def call(self, x):
x = self.conv1(x)
x = self.bn(x)
x = self.relu(x)
x = self.mp1(x)

x = self.conv2_1(x)
x = self.conv2_2(x)
x = self.conv2_3(x)

x = self.conv3_1(x)
x = self.conv3_2(x)
x = self.conv3_3(x)
x = self.conv3_4(x)

x = self.conv4_1(x)
x = self.conv4_2(x)
x = self.conv4_3(x)
x = self.conv4_4(x)
x = self.conv4_5(x)
x = self.conv4_6(x)

x = self.conv5_1(x)
x = self.conv5_2(x)
x = self.conv5_3(x)

x = self.pool(x)
x = self.fc1(x)
x = self.dp1(x)
x = self.fc2(x)
x = self.dp2(x)
x = self.fc3(x)
return x


model = ResNet34()
model.build(input_shape=(1, 480, 480, 3))
model.summary()

Inception

已经有不少博客在科普Inception系列模型的区别A Simple Guide to the Versions of the Inception Network

首先提出该模型的是2014年的Going deeper with convolutions Inception V1(GoogleNet),然后又分别有了好几个变体:Inception V2,Inception V3,Inception V4Inception-ResNet-v2。和ResNet一样,Inception网络中一个重要的moduleInception Module

Inception Module

其中这些Network中被广泛使用的是Inception_v3Inception-ResNet-v2.

MobileNet

The idea behind MobileNet is to use depthwise separable convolutions to build loghter deep neural networks. In regular convolutional layer, the convolution kernel or filter is applied to all of the channels of the input image, by doing weighted sum of the input pixels with the filter and then slides to the next input pixels across the images.MobileNet uses this regular convolution only in the first layer. The next layers are the depthwise separable convolutions which are the combination of the depthwise and pointwise convolution. The depthwise convolution does the convolution on each channel separately. If the image has three channels, therefore, the output image also has three channels. This depthwise convolution is used to filter the input channels. The next step is the pointwise convolution, which is similar to the regular convolution but with a 1x1 filter. The purpose of pointwise convolution is to merge the output channels of the depthwise convolution to create new features. By doing so, the computational work needed to be done is less than the regular convolutional networks.

引用自the-differences-between-inception-resnet-and-mobilenet

MobileNet使用了两种卷积形式,depthwisepointwise,后者就是我们常见的卷积操作,只是使用的是1✖1的卷积核,input image有多少个channelfilter就会延展为几个channel,比如输入进来的channel数是3,那么一个3*3大小的filter就会extend成3✖3✖3的一个立方体,然后这27个数分别禹输入image对应的区域做乘积之后相加取和。但是depthwise卷积是对每一个channel分别做卷积,如果输入图片有三个channel,那么输出的也会是三个channel。如图:

depthwise convolution

tensorflow中有DepthwiseConv2D这个layer,它对于depthwise convolution的解释是:

Depthwise convolution is a type of convolution in which each input channel is convolved with a different kernel (called a depthwise kernel). You can understand depthwise convolution as the first step in a depthwise separable convolution.

MobileNetV2主要引进了Inverted residualslinear bottlenecks去解决在depthwise卷积操作中卷积核的参数往往是0的问题

other topics

在阅读Review of Image Classification Algorithms Based on Convolutional Neural Networks的最后一章节时,作者不仅介绍了现在research和industry领域用的比较多的image classification的模型,也给出了各个模型在image-net数据集上的accuracy。在总结章,作者还提出了一些结论性的发现,我觉得蛮收益的,将文章的观点整理在这里。

  1. 2012年到2017年主要提供了日后用于分类的basic CNN模型架构,这期间的模型架构有2012的alexnet,2014年的vgg,2014年的inception,2015年的resnet,2017年提出了attention加cnn的架构
  2. attention加入到cnn之后形成了新的模型,也因此提高了模型的performance。现在很多模型会将SE block嵌入到模型架构中去,我查了下这个SE block是SEnet中的一个block,squeeze and excitation block。SEnet是在2017年提出的,这个知识点待补充
  3. 超参数的选择对于CNN网络的performance影响很大,很多的工作在着力于减少超参数的个数以及replace them with other composite coefficients。
  4. 手动设计一个performance很好的网络往往需要很多effort,NAS search (neural architecture search)可以让这个过程变得更简单
  5. 想要提升模型的performance,不仅仅需要将关注力放在模型架构的设计上,data augmentation,transfer learning,training strategies也可以帮助我们提高模型的准确度。在transfer learning上,paper: Large Scale Learning of General Visual Representations for Transfer 总结了一些在不同的task上如何利用transfer learning取得很好的performance的办法。

CNN model 还面临的挑战:

  1. lightweight models比如mobileNet系列的轻量级模型往往需要牺牲accuracy来提高efficiency。未来在embedded系统上,CNN的运行效率值得去explore
  2. cnn模型在semi-supervised和unsupervised上的发挥不如NLP领域。

future directions:

  1. 重视vision transformer. 如何将卷积和transformer有效结合起来是当前的一个热点。目前的SOTA network是 CoAtNet,在image net数据集上的accuracy是90.88,确实是目前在image net数据集上performance最高的模型架构。值得读一下,mark!
  2. 有一些关于CNN的传统技术可能会成为阻碍CNN发展的重要因素,诸如:activation function的选择,dropout,batch normalization。

SENet 2017

原文 SEnet,是由自动驾驶公司Momenta在2017年公布的一种全新的图像识别结构,它通过对特征通道间的相关性进行建模,把重要的特征进行强化来提升准确率。这个结构是2017 ILSVR竞赛的冠军,top5的错误率达到了2.251%,比2016年的第一名还要低25%,可谓提升巨大。

CoAtNet

Reference

  1. Architecture comparison of AlexNet, VGGNet, ResNet, Inception, DenseNet
  2. VGGNet Architecture Explained
  3. resnet-residual-neural-network Resnet系列网络架构的区别