1.模型結構
本案例整體采用transformer論文中提出的結構,部分設置做了調整。transformer
網絡結構介紹可參考博客——入門級別的Transformer模型介紹,這里著重介紹其代碼實現。
模型的整體結構,包括詞嵌入層,位置編碼,編碼器,解碼器、輸出層
部分。
2.詞嵌入層
詞嵌入層用于將token
轉化為詞向量
,該層可直接調用nn模塊
中的Embedding
方法。該方法主要包括兩個參數,分別表示詞表的大小(vocab_size
)和詞嵌入的維度(emb_size
),同時為了訓練更穩定,加入了縮放因子dk\sqrt {d_k}dk??,代碼如下:
class TokenEmbedding(nn.Module):def __init__(self, vocab_size: int, emb_size):super(TokenEmbedding, self).__init__()# 詞嵌入層:將詞索引映射到emb_size維的向量self.embedding = nn.Embedding(vocab_size, emb_size)# 記錄嵌入維度(用于縮放)self.emb_size = emb_sizedef forward(self, tokens: Tensor):# 將詞索引轉換為詞向量,并乘以√emb_size(縮放,穩定梯度)return self.embedding(tokens.long()) * math.sqrt(self.emb_size)
3.位置編碼
位置編碼層用于給序列添加位置信息,解決自注意力機制無法感知序列順序的問題。公式為:
PE(pos,2i)=sin(pos10002id)PE(pos,2i)=sin(\frac{pos}{1000\frac{2i}{d}})PE(pos,2i)=sin(1000d2i?pos?)
PE(pos,2i+1)=cos(pos10002id)PE(pos,2i+1)=cos(\frac{pos}{1000\frac{2i}{d}})PE(pos,2i+1)=cos(1000d2i?pos?)
代碼表示如下:
class PositionalEncoding(nn.Module):def __init__(self, emb_size: int, dropout, maxlen: int = 5000):super(PositionalEncoding, self).__init__()# 計算位置編碼的衰減因子(控制正弦/余弦函數的頻率)den = torch.exp(- torch.arange(0, emb_size, 2) * math.log(10000) / emb_size)# 位置索引(0到maxlen-1)pos = torch.arange(0, maxlen).reshape(maxlen, 1)# 初始化位置編碼矩陣(形狀:[maxlen, emb_size])pos_embedding = torch.zeros((maxlen, emb_size))# 偶數列用正弦函數填充(pos * den)pos_embedding[:, 0::2] = torch.sin(pos * den)# 奇數列用余弦函數填充(pos * den)pos_embedding[:, 1::2] = torch.cos(pos * den)# 調整維度(添加批次維度,便于與詞嵌入向量相加)pos_embedding = pos_embedding.unsqueeze(-2)# Dropout層(正則化,防止過擬合)self.dropout = nn.Dropout(dropout)# 注冊為緩沖區(模型保存/加載時自動處理)self.register_buffer('pos_embedding', pos_embedding)def forward(self, token_embedding: Tensor):# 將詞嵌入向量與位置編碼相加,并應用Dropoutreturn self.dropout(token_embedding + self.pos_embedding[:token_embedding.size(0),:])
4.編碼器
由于編碼器部分是通過堆疊多個子編碼器層所構成的,子編碼器包括:多頭自注意力層、殘差連接與歸一化、前饋網絡三部分,該部分代碼全部被封裝成TransformerEncoderLayer
函數中,使用時只需要傳遞相應超參數即可,如詞嵌入維度、多頭注意力的頭數、前饋網絡的隱含層維度,代碼實現為:
# 定義編碼器層(單頭注意力→多頭注意力→前饋網絡)
encoder_layer = TransformerEncoderLayer(d_model=emb_size, # 輸入特征維度(與詞嵌入維度一致)nhead=NHEAD, # 多頭注意力的頭數dim_feedforward=dim_feedforward # 前饋網絡隱藏層維度
)
# 堆疊多層編碼器層形成完整編碼器
self.transformer_encoder = TransformerEncoder(encoder_layer, num_layers=num_encoder_layers)
5.解碼器
解碼器同編碼器類似,代碼可以表述為:
# 定義解碼器層(掩碼多頭注意力→編碼器-解碼器多頭注意力→前饋網絡)
decoder_layer = TransformerDecoderLayer(d_model=emb_size, # 輸入特征維度(與詞嵌入維度一致)nhead=NHEAD, # 多頭注意力頭數(與編碼器一致)dim_feedforward=dim_feedforward # 前饋網絡隱藏層維度
)
# 堆疊多層解碼器層形成完整解碼器
self.transformer_decoder = TransformerDecoder(decoder_layer, num_layers=num_decoder_layers)
6.輸出層
輸出通過線性層得到每個單詞的得分,可直接通過Linear
層直接實現。
7.大體代碼
基于上述介紹,完整代碼如下:
from torch.nn import (TransformerEncoder, TransformerDecoder,TransformerEncoderLayer, TransformerDecoderLayer)class Seq2SeqTransformer(nn.Module):"""基于Transformer的序列到序列翻譯模型(日中機器翻譯核心模塊)包含編碼器(處理源語言序列)和解碼器(生成目標語言序列)"""def __init__(self, num_encoder_layers: int, num_decoder_layers: int,emb_size: int, src_vocab_size: int, tgt_vocab_size: int,dim_feedforward: int = 512, dropout: float = 0.1):"""初始化Transformer模型參數和組件:param num_encoder_layers: 編碼器層數(論文中通常為6,此處根據計算資源調整):param num_decoder_layers: 解碼器層數(與編碼器層數一致):param emb_size: 詞嵌入維度(對應Transformer的d_model,需與多頭注意力維度匹配):param src_vocab_size: 源語言(日語)詞表大小:param tgt_vocab_size: 目標語言(中文)詞表大小:param dim_feedforward: 前饋網絡隱藏層維度(通常為4*d_model):param dropout: dropout概率(用于正則化,防止過擬合)"""super(Seq2SeqTransformer, self).__init__()# 定義編碼器層(單頭注意力→多頭注意力→前饋網絡)encoder_layer = TransformerEncoderLayer(d_model=emb_size, # 輸入特征維度(與詞嵌入維度一致)nhead=NHEAD, # 多頭注意力的頭數(需滿足 emb_size % nhead == 0)dim_feedforward=dim_feedforward # 前饋網絡隱藏層維度)# 堆疊多層編碼器層形成完整編碼器self.transformer_encoder = TransformerEncoder(encoder_layer, num_layers=num_encoder_layers)# 定義解碼器層(掩碼多頭注意力→編碼器-解碼器多頭注意力→前饋網絡)decoder_layer = TransformerDecoderLayer(d_model=emb_size, # 輸入特征維度(與詞嵌入維度一致)nhead=NHEAD, # 多頭注意力頭數(與編碼器一致)dim_feedforward=dim_feedforward # 前饋網絡隱藏層維度)# 堆疊多層解碼器層形成完整解碼器self.transformer_decoder = TransformerDecoder(decoder_layer, num_layers=num_decoder_layers)# 生成器:將解碼器輸出映射到目標詞表(預測每個位置的目標詞)self.generator = nn.Linear(emb_size, tgt_vocab_size)# 源語言詞嵌入層(將詞索引轉換為連續向量)self.src_tok_emb = TokenEmbedding(src_vocab_size, emb_size)# 目標語言詞嵌入層(與源語言共享嵌入層可提升效果,此處未共享)self.tgt_tok_emb = TokenEmbedding(tgt_vocab_size, emb_size)# 位置編碼層(注入序列位置信息,解決Transformer的位置無關性)self.positional_encoding = PositionalEncoding(emb_size, dropout=dropout)def forward(self, src: Tensor, trg: Tensor, src_mask: Tensor,tgt_mask: Tensor, src_padding_mask: Tensor,tgt_padding_mask: Tensor, memory_key_padding_mask: Tensor):"""前向傳播(訓練時使用教師強制,輸入完整目標序列):param src: 源語言序列張量(形狀:[seq_len, batch_size]):param trg: 目標語言序列張量(形狀:[seq_len, batch_size]):param src_mask: 源序列注意力掩碼(形狀:[seq_len, seq_len],全0表示無掩碼):param tgt_mask: 目標序列掩碼(下三角掩碼,防止關注未來詞):param src_padding_mask: 源序列填充掩碼(標記<pad>位置,形狀:[batch_size, seq_len]):param tgt_padding_mask: 目標序列填充掩碼(標記<pad>位置,形狀:[batch_size, seq_len]):param memory_key_padding_mask: 編碼器輸出的填充掩碼(與src_padding_mask一致):return: 目標序列的詞表概率分布(形狀:[seq_len, batch_size, tgt_vocab_size])"""# 源序列處理:詞嵌入 + 位置編碼src_emb = self.positional_encoding(self.src_tok_emb(src))# 目標序列處理:詞嵌入 + 位置編碼(訓練時使用教師強制,輸入完整目標序列)tgt_emb = self.positional_encoding(self.tgt_tok_emb(trg))# 編碼器處理源序列,生成記憶向量(memory)memory = self.transformer_encoder(src_emb, src_mask, src_padding_mask)# 解碼器利用記憶向量生成目標序列outs = self.transformer_decoder(tgt_emb, # 目標序列嵌入(含位置信息)memory, # 編碼器輸出的記憶向量tgt_mask, # 目標序列掩碼(防止未來詞)None, # 編碼器-解碼器注意力掩碼(此處未使用)tgt_padding_mask, # 目標序列填充掩碼(忽略<pad>)memory_key_padding_mask # 記憶向量填充掩碼(與源序列填充掩碼一致))# 通過生成器輸出目標詞表的概率分布return self.generator(outs)def encode(self, src: Tensor, src_mask: Tensor):"""編碼源序列(推理時單獨調用,生成編碼器記憶向量):param src: 源語言序列張量(形狀:[seq_len, batch_size]):param src_mask: 源序列注意力掩碼(形狀:[seq_len, seq_len]):return: 編碼器輸出的記憶向量(形狀:[seq_len, batch_size, emb_size])"""return self.transformer_encoder(self.positional_encoding(self.src_tok_emb(src)), # 源序列嵌入+位置編碼src_mask # 源序列注意力掩碼)def decode(self, tgt: Tensor, memory: Tensor, tgt_mask: Tensor):"""解碼目標序列(推理時逐步生成目標詞):param tgt: 當前已生成的目標序列前綴(形狀:[current_seq_len, batch_size]):param memory: 編碼器輸出的記憶向量(形狀:[seq_len, batch_size, emb_size]):param tgt_mask: 目標序列掩碼(下三角掩碼,防止關注未來詞):return: 解碼器輸出(形狀:[current_seq_len, batch_size, emb_size])"""return self.transformer_decoder(self.positional_encoding(self.tgt_tok_emb(tgt)), # 目標前綴嵌入+位置編碼memory, # 編碼器記憶向量tgt_mask # 目標前綴掩碼(僅允許關注已生成部分))
class PositionalEncoding(nn.Module):def __init__(self, emb_size: int, dropout, maxlen: int = 5000):super(PositionalEncoding, self).__init__()# 計算位置編碼的衰減因子(控制正弦/余弦函數的頻率)den = torch.exp(- torch.arange(0, emb_size, 2) * math.log(10000) / emb_size)# 位置索引(0到maxlen-1)pos = torch.arange(0, maxlen).reshape(maxlen, 1)# 初始化位置編碼矩陣(形狀:[maxlen, emb_size])pos_embedding = torch.zeros((maxlen, emb_size))# 偶數列用正弦函數填充(pos * den)pos_embedding[:, 0::2] = torch.sin(pos * den)# 奇數列用余弦函數填充(pos * den)pos_embedding[:, 1::2] = torch.cos(pos * den)# 調整維度(添加批次維度,便于與詞嵌入向量相加)pos_embedding = pos_embedding.unsqueeze(-2)# Dropout層(正則化,防止過擬合)self.dropout = nn.Dropout(dropout)# 注冊為緩沖區(模型保存/加載時自動處理)self.register_buffer('pos_embedding', pos_embedding)def forward(self, token_embedding: Tensor):# 將詞嵌入向量與位置編碼相加,并應用Dropoutreturn self.dropout(token_embedding + self.pos_embedding[:token_embedding.size(0),:])class TokenEmbedding(nn.Module):def __init__(self, vocab_size: int, emb_size):super(TokenEmbedding, self).__init__()# 詞嵌入層:將詞索引映射到emb_size維的向量self.embedding = nn.Embedding(vocab_size, emb_size)# 記錄嵌入維度(用于縮放)self.emb_size = emb_sizedef forward(self, tokens: Tensor):# 將詞索引轉換為詞向量,并乘以√emb_size(縮放,穩定梯度)return self.embedding(tokens.long()) * math.sqrt(self.emb_size)
結語
至此,模型已完成搭建,后續博客將繼續介紹模型訓練部分的內容,希望本篇博客能夠對你理解transformer有所幫助!