從源碼學(xué)習(xí)Transformer!
點(diǎn)擊上方“小白學(xué)視覺”,選擇加"星標(biāo)"或“置頂”
重磅干貨,第一時(shí)間送達(dá)
本文轉(zhuǎn)自|機(jī)器學(xué)習(xí)算法工程師
Transformer總體結(jié)構(gòu)
近幾年NLP領(lǐng)域有了突飛猛進(jìn)的發(fā)展,預(yù)訓(xùn)練模型功不可沒。當(dāng)前利用預(yù)訓(xùn)練模型(pretrain models)在下游任務(wù)中進(jìn)行fine-tune,已經(jīng)成為了大部分NLP任務(wù)的固定范式。Transformer摒棄了RNN的序列結(jié)構(gòu),完全采用attention和全連接,嚴(yán)格來說不屬于預(yù)訓(xùn)練模型。但它卻是當(dāng)前幾乎所有pretrain models的基本結(jié)構(gòu),為pretrain models打下了堅(jiān)實(shí)的基礎(chǔ),并逐步發(fā)展出了transformer-XL,reformer等優(yōu)化架構(gòu)。本文結(jié)合論文和源碼,對transformer基本結(jié)構(gòu),進(jìn)行詳細(xì)分析。
Transformer是谷歌在2017年6月提出,發(fā)表在NIPS2017上。論文地址
分析的代碼為Harvardnlp的代碼,基于PyTorch, 地址
Transformer主體框架是一個(gè)encoder-decoder結(jié)構(gòu),去掉了RNN序列結(jié)構(gòu),完全基于attention和全連接。在WMT2014英語翻譯德語任務(wù)上,bleu值達(dá)到了28.4,達(dá)到當(dāng)時(shí)的SOTA。其總體結(jié)構(gòu)如下所示

總體為一個(gè)典型的encoder-decoder結(jié)構(gòu)。代碼如下
# 整個(gè)模型入口
def make_model(src_vocab, tgt_vocab, N=6,
d_model=512, d_ff=2048, h=8, dropout=0.1):
"Helper: Construct a model from hyperparameters."
c = copy.deepcopy
# multiHead attention
attn = MultiHeadedAttention(h, d_model)
# feed-forward
ff = PositionwiseFeedForward(d_model, d_ff, dropout)
# position-encoding
position = PositionalEncoding(d_model, dropout)
# 整體為一個(gè)encoder-decoder
model = EncoderDecoder(
# encoder編碼層
Encoder(EncoderLayer(d_model, c(attn), c(ff), dropout), N),
# decoder解碼層
Decoder(DecoderLayer(d_model, c(attn), c(attn), c(ff), dropout), N),
# 編碼層輸入,輸入語句進(jìn)行token embedding和position embedding
nn.Sequential(Embeddings(d_model, src_vocab), c(position)),
# 解碼層輸入,同樣需要做token embedding和position embedding
nn.Sequential(Embeddings(d_model, tgt_vocab), c(position)),
# linear + softmax,查找vocab中概率最大的字
Generator(d_model, tgt_vocab))
# This was important from their code.
# Initialize parameters with Glorot / fan_avg.
for p in model.parameters():
if p.dim() > 1:
nn.init.xavier_uniform(p)
return model
make_model為Transformer模型定義的入口,它先定義了multi-head attention、feed-forward、position-encoding等一系列子模塊,然后定義了一個(gè)encoder-decoder結(jié)構(gòu)并返回。下面來看encoder-decoder定義。
class EncoderDecoder(nn.Module):
"""
一個(gè)標(biāo)準(zhǔn)的encoder和decoder框架,可以自定義embedding、encoder、decoder等
"""
def __init__(self, encoder, decoder, src_embed, tgt_embed, generator):
super(EncoderDecoder, self).__init__()
# encoder和decoder通過構(gòu)造函數(shù)傳入,可靈活更改
self.encoder = encoder
self.decoder = decoder
# src和target的embedding,也是通過構(gòu)造函數(shù)傳入,方便靈活更改
self.src_embed = src_embed
self.tgt_embed = tgt_embed
# linear + softmax
self.generator = generator
def forward(self, src, tgt, src_mask, tgt_mask):
"Take in and process masked src and target sequences."
# 先對輸入進(jìn)行encode,然后再通過decode輸出
return self.decode(self.encode(src, src_mask), src_mask,
tgt, tgt_mask)
def encode(self, src, src_mask):
# 先對輸入進(jìn)行embedding,然后再經(jīng)過encoder
return self.encoder(self.src_embed(src), src_mask)
def decode(self, memory, src_mask, tgt, tgt_mask):
# 先對目標(biāo)進(jìn)行embedding,然后經(jīng)過decoder
return self.decoder(self.tgt_embed(tgt), memory, src_mask, tgt_mask)
encoder-decoder定義了一個(gè)標(biāo)準(zhǔn)的編碼解碼框架,其中編碼器、解碼器均可以自定義,有很強(qiáng)的泛化能力。模塊運(yùn)行時(shí)會調(diào)用forward函數(shù),它先對輸入進(jìn)行encode,然后再通過decode輸出。我們就不詳細(xì)展開了。
2 encoder
2.1 encoder定義
encoder分為兩部分
輸入層embedding。輸入層對inputs文本做token embedding,并對每個(gè)字做position encoding,然后疊加在一起,作為最終的輸入。
編碼層encoding。編碼層是多層結(jié)構(gòu)相同的layer堆疊而成。每個(gè)layer又包括兩部分,multi-head self-attention和feed-forward全連接,并在每部分加入了殘差連接和歸一化。
代碼實(shí)現(xiàn)上也驗(yàn)證了這一點(diǎn)。我們看EncoderDecoder類中的encode函數(shù),它先利用輸入embedding層對原始輸入進(jìn)行embedding,然后再通過編碼層進(jìn)行encoding。
class EncoderDecoder(nn.Module):
def encode(self, src, src_mask):
# 先對輸入進(jìn)行embedding,然后再經(jīng)過encoder
return self.encoder(self.src_embed(src), src_mask)
2.2 輸入層embedding
原始文本經(jīng)過embedding層進(jìn)行向量化,它包括token embedding和position embedding兩層。
2.2.1 token embedding
token embedding對文本進(jìn)行向量化,一般來說有兩種方式
采用固定詞向量,比如利用Word2vec預(yù)先訓(xùn)練好的。這種方式是LSTM時(shí)代常用的方式,比較簡單省事,無需訓(xùn)練。但由于詞向量是固定的,不能解決一詞多義的問題,詞語本身也不是contextual的,沒有結(jié)合上下文語境信息,另外對于不在詞向量中的詞語,比如特定領(lǐng)域詞語或者新詞,容易出現(xiàn)OOV問題。
隨機(jī)初始化,然后訓(xùn)練。這種方式比較麻煩,需要大規(guī)模訓(xùn)練語料,但能解決固定詞向量的一系列問題。Transformer采用了這種方式。
另外,基于Transformer的BERT模型在中文處理時(shí),直接基于字做embedding,優(yōu)點(diǎn)有
無需分詞,故不會引入分詞誤差。事實(shí)上,只要訓(xùn)練語料充分,模型自然就可以學(xué)到分詞信息了。
中文字個(gè)數(shù)固定,不會導(dǎo)致OOV問題
中文字相對詞,數(shù)量少很多,embedding層參數(shù)大大縮小,減小了模型體積,并加快了訓(xùn)練速度。
事實(shí)上,就算在LSTM時(shí)代,很多case中,我們也碰到過基于字的embedding的效果比基于詞的要好一些。
class Embeddings(nn.Module):
# token embedding,隨機(jī)初始化訓(xùn)練,然后查表找到每個(gè)字的embedding
def __init__(self, d_model, vocab):
super(Embeddings, self).__init__()
# 構(gòu)建一個(gè)隨機(jī)初始化的詞向量表,[vocab_size, d_model]。bert中的設(shè)置為[21128, 768]
self.lut = nn.Embedding(vocab, d_model)
self.d_model = d_model
def forward(self, x):
# 從詞向量表中查找字對應(yīng)的embedding向量
return self.lut(x) * math.sqrt(self.d_model)
由代碼可見,Transformer采用的是隨機(jī)初始化,然后訓(xùn)練的方式。詞向量維度為[vocab_size, d_model]。例如BERT中為[21128, 768],參數(shù)量還是很大的。ALBert針對embedding層進(jìn)行矩陣分解,大大減小了embedding層體積。
2.2.2 position encoding
首先一個(gè)問題,為啥要進(jìn)行位置編碼呢。原因在于self-attention,將任意兩個(gè)字之間距離縮小為1,丟失了字的位置信息,故我們需要加上這一信息。我們也可以想到兩種方法
固定編碼。Transformer采用了這一方式,通過奇數(shù)列cos函數(shù),偶數(shù)列sin函數(shù)方式,利用三角函數(shù)對位置進(jìn)行固定編碼。
動態(tài)訓(xùn)練。BERT采用了這種方式。先隨機(jī)初始化一個(gè)embedding table,然后訓(xùn)練得到table 參數(shù)值。predict時(shí)通過embedding_lookup找到每個(gè)位置的embedding。這種方式和token embedding類似。
哪一種方法好呢?個(gè)人以為各有利弊
固定編碼方式簡潔,不需要訓(xùn)練。且不受embedding table維度影響,理論上可以支持任意長度文本。(但要盡量避免預(yù)測文本很長,但訓(xùn)練集文本較短的case)
動態(tài)訓(xùn)練方式,在語料比較大時(shí),準(zhǔn)確度比較好。但需要訓(xùn)練,且最致命的是,限制了輸入文本長度。當(dāng)文本長度大于position embedding table維度時(shí),超出的position無法查表得到embedding(可以理解為OOV了)。這也是為什么BERT模型文本長度最大512的原因。
class PositionalEncoding(nn.Module):
# 位置編碼。transformer利用編碼方式實(shí)現(xiàn),無需訓(xùn)練。bert則采用訓(xùn)練embedding_lookup方式
# 編碼方式文本語句長度不受限,但準(zhǔn)確度不高
# 訓(xùn)練方式文本長度會受position維度限制(這也是為什么bert只能處理最大512個(gè)字原因),但訓(xùn)練數(shù)據(jù)多時(shí),準(zhǔn)確率高
def __init__(self, d_model, dropout, max_len=5000):
super(PositionalEncoding, self).__init__()
self.dropout = nn.Dropout(p=dropout)
# 采用sin和cos進(jìn)行position encoding
pe = torch.zeros(max_len, d_model)
position = torch.arange(0, max_len).unsqueeze(1)
div_term = torch.exp(torch.arange(0, d_model, 2) *
-(math.log(10000.0) / d_model))
pe[:, 0::2] = torch.sin(position * div_term) # 偶數(shù)列
pe[:, 1::2] = torch.cos(position * div_term) # 奇數(shù)列
pe = pe.unsqueeze(0)
self.register_buffer('pe', pe)
def forward(self, x):
# token embedding和position encoding加在一起
x = x + Variable(self.pe[:, :x.size(1)],
requires_grad=False)
return self.dropout(x)
由代碼可見,position encoding直接采用了三角函數(shù)。對偶數(shù)列采用sin,奇數(shù)列采用cos。

Encoder層是Transformer的核心,它由N層相同結(jié)構(gòu)的layer(默認(rèn)6層)堆疊而成。
class Encoder(nn.Module):
"Core encoder is a stack of N layers"
def __init__(self, layer, N):
super(Encoder, self).__init__()
# N層堆疊而成,每一層結(jié)構(gòu)都是相同的,訓(xùn)練參數(shù)不同
self.layers = clones(layer, N)
# layer normalization
self.norm = LayerNorm(layer.size)
def forward(self, x, mask):
# 1 經(jīng)過N層堆疊的multi-head attention + feed-forward
for layer in self.layers:
x = layer(x, mask)
# 2 對encoder最終輸出結(jié)果進(jìn)行l(wèi)ayer-norm歸一化。層間和層內(nèi)子模塊都做過 add + dropout + layer-norm
return self.norm(x)
encoder的定義很簡潔。先經(jīng)過N層相同結(jié)構(gòu)的layer,然后再進(jìn)行歸一化輸出。重點(diǎn)我們來看layer的定義。
class EncoderLayer(nn.Module):
"Encoder is made up of self-attn and feed forward (defined below)"
def __init__(self, size, self_attn, feed_forward, dropout):
super(EncoderLayer, self).__init__()
# 1 self_attention
self.self_attn = self_attn
# 2 feed_forward
self.feed_forward = feed_forward
# 3 殘差連接。encoder和decoder,每層結(jié)構(gòu),每個(gè)子結(jié)構(gòu),都有殘差連接。
# add + drop-out + layer-norm
self.sublayer = clones(SublayerConnection(size, dropout), 2)
self.size = size
def forward(self, x, mask):
# 經(jīng)過self_attention, 然后和輸入進(jìn)行add + layer-norm
x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, mask))
# 經(jīng)過feed_forward, 此模塊也有add + layer-norm
return self.sublayer[1](x, self.feed_forward)
encoder layer分為兩個(gè)子模塊
self attention, 并對輸入attention前的和經(jīng)過attention輸出的,做殘差連接。殘差連接先經(jīng)過layer-norm歸一化,然后進(jìn)行dropout,最后再做add。后面我們詳細(xì)分析
feed-forward全連接,也有殘差連接的存在,方式和self attention相同。
2.3.1 MultiHeadedAttention
MultiHeadedAttention采用多頭self-attention。它先將隱向量切分為h個(gè)頭,然后每個(gè)頭內(nèi)部進(jìn)行self-attention計(jì)算,最后再concat再一起。

代碼如下
class MultiHeadedAttention(nn.Module):
def __init__(self, h, d_model, dropout=0.1):
super(MultiHeadedAttention, self).__init__()
assert d_model % h == 0
# d_model為隱層維度,也是embedding的維度,h為多頭個(gè)數(shù)。
# d_k為每個(gè)頭的隱層維度,要除以多頭個(gè)數(shù)。也就是加入了多頭,總隱層維度不變。
self.d_k = d_model // h
self.h = h
# 線性連接
self.linears = clones(nn.Linear(d_model, d_model), 4)
self.attn = None
self.dropout = nn.Dropout(p=dropout)
def forward(self, query, key, value, mask=None):
if mask is not None:
# 輸入mask,在decoder的時(shí)候有用到。decode時(shí)不能看到要生成字之后的字,所以需要mask
mask = mask.unsqueeze(1)
nbatches = query.size(0)
# 1) q, k, v形狀變化,加入多頭, [batch, L, d_model] => [batch, h, L, d_model/h]
query, key, value = [l(x).view(nbatches, -1, self.h, self.d_k).transpose(1, 2)
for l, x in zip(self.linears, (query, key, value))]
# 2) attention計(jì)算
x, self.attn = attention(query, key, value, mask=mask,
dropout=self.dropout)
# 3) 多頭結(jié)果concat在一起,還原為初始形狀
x = x.transpose(1, 2).contiguous().view(nbatches, -1, self.h * self.d_k)
# 4)最后經(jīng)過一個(gè)線性層
return self.linears[-1](x)
下面重點(diǎn)來看單個(gè)頭的self-attention。也就是論文中的“Scaled Dot-Product Attention”。attention本質(zhì)上是一個(gè)向量的加權(quán)求和。它探討的是每個(gè)位置對當(dāng)前位置的貢獻(xiàn)。步驟如下
q向量和每個(gè)位置的k向量計(jì)算點(diǎn)積,然后除以向量長度的根號。計(jì)算點(diǎn)積可以認(rèn)為是進(jìn)行權(quán)重計(jì)算。除以向量長度原因是向量越長,q*k值理論上會越大,故需要在向量長度上做歸一化。
attention-mask。mask和輸入矩陣shape相同,mask矩陣中值為0位置對應(yīng)的輸入矩陣的值更改為-1e9,一個(gè)非常非常小的數(shù),經(jīng)過softmax后趨近于0。decoder中使用了mask,后面我們詳細(xì)分析。
softmax歸一化,使得q向量和每個(gè)位置的k向量的score分布到(0, 1)之間
加權(quán)系數(shù)乘以每個(gè)位置v向量,然后加起來。
公式如下:

代碼如下
def attention(query, key, value, mask=None, dropout=None):
# attention計(jì)算,self_attention和soft-attention都是使用這個(gè)函數(shù)
# self-attention, q k v 均來自同一文本。要么是encoder,要么是decoder
# soft-attention, q來自decoder,k和v來自encoder,從而按照decoder和encoder相關(guān)性,將encoder信息融合進(jìn)來
d_k = query.size(-1)
# 利用q * k計(jì)算兩向量間相關(guān)度,相關(guān)度高則權(quán)重大。
# 除以根號dk的原因是,對向量長度進(jìn)行歸一化。q和k的向量長度越長,q*k的值越大
scores = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(d_k)
# attention-mask,將 mask中為1的 元素所在的索引,在a中相同的的索引處替換為 value
if mask is not None:
scores = scores.masked_fill(mask == 0, -1e9)
# softmax歸一化
p_attn = F.softmax(scores, dim = -1)
# dropout
if dropout is not None:
p_attn = dropout(p_attn)
# 最后利用歸一化后的加權(quán)系數(shù),乘以每一個(gè)v向量,再加和在一起,作為attention后的向量。每個(gè)字對應(yīng)一個(gè)向量
return torch.matmul(p_attn, value), p_attn
self-attention和soft-attention共用了這個(gè)函數(shù),他們之間的唯一區(qū)別是q k v向量的來源不同。self-attention中q k v 均來自同一文本。而decoder的soft-attention,q來自于decoder,k和v來自于encoder。它體現(xiàn)的是encoder對decoder的加權(quán)貢獻(xiàn)。
2.3.2 PositionwiseFeedForward
feed-forward本質(zhì)是一個(gè)兩層的全連接,全連接之間加入了relu非線性和dropout。比較簡單,代碼如下
class PositionwiseFeedForward(nn.Module):
# 全連接層
def __init__(self, d_model, d_ff, dropout=0.1):
super(PositionwiseFeedForward, self).__init__()
# 第一層全連接 [d_model, d_ff]
self.w_1 = nn.Linear(d_model, d_ff)
# 第二層全連接 [d_ff, d_model]
self.w_2 = nn.Linear(d_ff, d_model)
# dropout
self.dropout = nn.Dropout(dropout)
def forward(self, x):
# 全連接1 -> relu -> dropout -> 全連接2
return self.w_2(self.dropout(F.relu(self.w_1(x))))
總體過程是:全連接1 -> relu -> dropout -> 全連接2。兩層全連接內(nèi)部沒有shortcut,這兒不要搞混了。
2.3.3 SublayerConnection
在每層的self-attention和feed-forward模塊中,均應(yīng)用了殘差連接。殘差連接先對輸入進(jìn)行l(wèi)ayerNorm歸一化,然后送入attention或feed-forward模塊,然后經(jīng)過dropout,最后再和原始輸入相加。這樣做的好處是,讓每一層attention和feed-forward模塊的輸入值,均是經(jīng)過歸一化的,保持在一個(gè)量級上,從而可以加快收斂速度。
class SublayerConnection(nn.Module):
"""
A residual connection followed by a layer norm.
Note for code simplicity the norm is first as opposed to last.
"""
def __init__(self, size, dropout):
super(SublayerConnection, self).__init__()
# layer-norm 歸一化
self.norm = LayerNorm(size)
# dropout
self.dropout = nn.Dropout(dropout)
def forward(self, x, sublayer):
# 先對輸入進(jìn)行l(wèi)ayer-norm, 然后經(jīng)過attention等相關(guān)模塊,再經(jīng)過dropout,最后再和輸入相加
return x + self.dropout(sublayer(self.norm(x)))
從forward函數(shù)可見,先對輸入進(jìn)行l(wèi)ayer-norm, 然后經(jīng)過attention等相關(guān)模塊,再經(jīng)過dropout,最后再和輸入相加。殘差連接的作用就不說了,參考ResNet。
3 decoder
decoder結(jié)構(gòu)和encoder大體相同,也是堆疊了N層相同結(jié)構(gòu)的layer(默認(rèn)6層)。不同的是,decoder的每個(gè)子層包括三層。
masked multi-head self-attention。這一部分和encoder基本相同,區(qū)別在于decoder為了保證模型不能看見要預(yù)測字的后面位置的字,加入了mask,從而避免未來信息的穿越問題。mask為一個(gè)上三角矩陣,上三角全為1,下三角和對角線全為0
multi-head soft-attention。soft-attention和self-attention結(jié)構(gòu)基本相同,甚至實(shí)現(xiàn)函數(shù)都是同一個(gè)。唯一的區(qū)別在于,self-attention的q k v矩陣來自同一個(gè),所以叫self-attention。而soft-attention的q來自decoder,k和v來自encoder。表征的是encoder的整體輸出對于decoder的貢獻(xiàn)。
feed-forward。這一塊基本相同。
另外三個(gè)模塊均使用了殘差連接,步驟仍然為 layerNorm -> attention等模塊 -> dropout -> 和輸入進(jìn)行add decoder每個(gè)layer代碼如下
class DecoderLayer(nn.Module):
"Decoder is made of self-attn, src-attn, and feed forward (defined below)"
def __init__(self, size, self_attn, src_attn, feed_forward, dropout):
super(DecoderLayer, self).__init__()
self.size = size
# self-attention 自注意力
self.self_attn = self_attn
# soft-attenton, encoder的輸出對decoder的作用
self.src_attn = src_attn
# feed-forward 全連接
self.feed_forward = feed_forward
# 殘差連接
self.sublayer = clones(SublayerConnection(size, dropout), 3)
def forward(self, x, memory, src_mask, tgt_mask):
# memory為encoder最終輸出
m = memory
# 1 對decoder輸入做self-attention, 再和輸入做殘差連接
x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, tgt_mask))
# 2 對encoder輸出和decoder當(dāng)前進(jìn)行soft-attention,此處也有殘差連接
x = self.sublayer[1](x, lambda x: self.src_attn(x, m, m, src_mask))
# 3 feed-forward全連接,也有殘差連接
return self.sublayer[2](x, self.feed_forward)
4 輸出層
decoder的輸出作為最終輸出層的輸入,經(jīng)過兩步
linear線性連接,也即是w * x + b
softmax歸一化,向量長度等于vocabulary的長度,得到vocabulary中每個(gè)字的概率。利用beam-search等方法,即可得到生成結(jié)果。
這一層比較簡單,代碼如下
class Generator(nn.Module):
"Define standard linear + softmax generation step."
def __init__(self, d_model, vocab):
super(Generator, self).__init__()
self.proj = nn.Linear(d_model, vocab)
def forward(self, x):
# 先經(jīng)過linear線性層,然后經(jīng)過softmax得到歸一化概率分布
# 輸出向量長度等于vocabulary的維度
return F.log_softmax(self.proj(x), dim=-1)
5 總結(jié)
Transformer相比LSTM的優(yōu)點(diǎn)
完全的并行計(jì)算,Transformer的attention和feed-forward,均可以并行計(jì)算。而LSTM則依賴上一時(shí)刻,必須串行
減少長程依賴,利用self-attention將每個(gè)字之間距離縮短為1,大大緩解了長距離依賴問題
提高網(wǎng)絡(luò)深度。由于大大緩解了長程依賴梯度衰減問題,Transformer網(wǎng)絡(luò)可以很深,基于Transformer的BERT甚至可以做到24層。而LSTM一般只有2層或者4層。網(wǎng)絡(luò)越深,高階特征捕獲能力越好,模型performance也可以越高。
真正的雙向網(wǎng)絡(luò)。Transformer可以同時(shí)融合前后位置的信息,而雙向LSTM只是簡單的將兩個(gè)方向的結(jié)果相加,嚴(yán)格來說仍然是單向的。
可解釋性強(qiáng)。完全基于attention的Transformer,可以表達(dá)字與字之間的相關(guān)關(guān)系,可解釋性更強(qiáng)。
Transformer也不是一定就比LSTM好,它的缺點(diǎn)如下
文本長度很長時(shí),比如篇章級別,計(jì)算量爆炸。self-attention的計(jì)算量為O(n^2), n為文本長度。Transformer-xl利用層級方式,將計(jì)算速度提升了1800倍
Transformer位置信息只靠position encoding,效果比較一般。當(dāng)語句較短時(shí),比如小于10個(gè)字,Transformer效果不一定比LSTM好
Transformer參數(shù)量較大,在大規(guī)模數(shù)據(jù)集上,效果遠(yuǎn)好于LSTM。但在小規(guī)模數(shù)據(jù)集上,如果不是利用pretrain models,效果不一定有LSTM好。
下載1:OpenCV-Contrib擴(kuò)展模塊中文版教程
交流群
歡迎加入公眾號讀者群一起和同行交流,目前有SLAM、三維視覺、傳感器、自動駕駛、計(jì)算攝影、檢測、分割、識別、醫(yī)學(xué)影像、GAN、算法競賽等微信群(以后會逐漸細(xì)分),請掃描下面微信號加群,備注:”昵稱+學(xué)校/公司+研究方向“,例如:”張三 + 上海交大 + 視覺SLAM“。請按照格式備注,否則不予通過。添加成功后會根據(jù)研究方向邀請進(jìn)入相關(guān)微信群。請勿在群內(nèi)發(fā)送廣告,否則會請出群,謝謝理解~

