Datawhale AI 暑期夏令營 第四期Task3

qaz961501發表於2024-08-03

Transformer架構

Transformer是一種用於自然語言處理(NLP)和其他序列到序列(sequence-to-sequence)任務的深度學習模型架構,它在2017年由Vaswani等人首次提出。Transformer架構引入了自注意力機制(self-attention mechanism),這是一個關鍵的創新,使其在處理序列資料時表現出色。
以下是Transformer的一些重要組成部分和特點:

  • 自注意力機制(Self-Attention):這是Transformer的核心概念之一,它使模型能夠同時考慮輸入序列中的所有位置,而不是像迴圈神經網路(RNN)或卷積神經網路(CNN)一樣逐步處理。自注意力機制允許模型根據輸入序列中的不同部分來賦予不同的注意權重,從而更好地捕捉語義關係。
  • 多頭注意力(Multi-Head Attention):Transformer中的自注意力機制被擴充套件為多個注意力頭,每個頭可以學習不同的注意權重,以更好地捕捉不同型別的關係。多頭注意力允許模型並行處理不同的資訊子空間。
  • 堆疊層(Stacked Layers):Transformer通常由多個相同的編碼器和解碼器層堆疊而成。這些堆疊的層有助於模型學習複雜的特徵表示和語義。
  • 位置編碼(Positional Encoding):由於Transformer沒有內建的序列位置資訊,它需要額外的位置編碼來表達輸入序列中單詞的位置順序。
  • 殘差連線和層歸一化(Residual Connections and Layer Normalization):這些技術有助於減輕訓練過程中的梯度消失和爆炸問題,使模型更容易訓練。
  • 編碼器和解碼器:Transformer通常包括一個編碼器用於處理輸入序列和一個解碼器用於生成輸出序列,這使其適用於序列到序列的任務,如機器翻譯。

這裡重點說明一下自注意力機制:
自注意力的作用:隨著模型處理輸入序列的每個單詞,自注意力會關注整個輸入序列的所有單詞,幫助模型對本單詞更好地進行編碼。在處理過程中,自注意力機制會將對所有相關單詞的理解融入到我們正在處理的單詞中。更具體的功能如下:

  • 序列建模:自注意力可以用於序列資料(例如文字、時間序列、音訊等)的建模。它可以捕捉序列中不同位置的依賴關係,從而更好地理解上下文。這對於機器翻譯、文字生成、情感分析等任務非常有用。
  • 平行計算:自注意力可以平行計算,這意味著可以有效地在現代硬體上進行加速。相比於RNN和CNN等序列模型,它更容易在GPU和TPU等硬體上進行高效的訓練和推理。(因為在自注意力中可以並行的計算得分)
  • 長距離依賴捕捉:傳統的迴圈神經網路(RNN)在處理長序列時可能面臨梯度消失或梯度爆炸的問題。自注意力可以更好地處理長距離依賴關係,因為它不需要按順序處理輸入序列。
    下圖指出了自注意力的基本結構:

具體來說,自注意力機制有三個關鍵步驟:

  1. 計算相關性: 對於輸入序列中的每個元素,透過計算它與其他所有元素的相關性得分(注意力分數)。這些分數決定了每個元素對其他元素的關注程度,即哪些元素對當前元素更重要。
  2. 加權求和: 使用相關性分數來加權計算一個加權和,這個加權和可以看作是對其他所有元素的資訊聚合,而權重由相關性分數決定。
  3. 轉換和輸出: 將加權和透過線性變換和啟用函式(比如ReLU)進行轉換,生成最終的輸出表示。這個輸出不僅包含了元素本身的資訊,還融合了來自所有其他元素的資訊。

透過這種自注意力機制,Transformer能夠在輸入序列中建立複雜的依賴關係,無論是長距離的依賴還是短距離的依賴,都可以被有效地捕捉和利用。這種能力使得Transformer在處理自然語言處理任務(如機器翻譯、文字生成)時表現出色。


下面我們來舉一個例子:
假設我們有這樣一句話:"The cat sat on the mat."

  1. 輸入表示: 首先,將這句話分割成詞(tokens)或者子詞(subword units),比如:"The", "cat", "sat", "on", "the", "mat", "."
  2. 詞嵌入(Word Embeddings): 每個詞被對映到一個高維空間的向量表示,這些向量包含了詞的語義資訊。
  3. 位置編碼(Positional Encoding): 為了區分句子中不同位置的詞,Transformer引入了位置編碼,它將位置資訊加入到詞的向量表示中。
  4. 自注意力機制:
  • 計算相關性: 對於每個詞,計算它與所有其他詞的相關性分數,這些分數由點積計算得出。
  • 加權求和: 使用相關性分數來加權所有詞的表示,生成每個詞的新表示。這個過程允許每個詞聚焦於句子中其他詞的重要性,而不受它們在句子中位置的限制。
  1. 前饋神經網路(Feedforward Network): 對於每個詞的新表示,透過一個前饋神經網路進行轉換和非線性變換。
  2. 層歸一化(Layer Normalization)和殘差連線(Residual Connection): 在每個子層的處理過程中,透過層歸一化來穩定訓練,同時保留殘差連線以避免資訊丟失。
  3. 輸出層: 最後,將經過多層Transformer編碼器處理後的表示送入輸出層,進行具體任務的預測或生成。

因此,在解決本題的時候,我們使用Transformer的Encoder部分,編碼我們的SMILES表示式。然後,再將等到的向量z透過一個線性層,輸出一個值。我們期望透過模型的學習,這個輸出的值就是該化學反應的產率。這樣就實現了透過Encoder將SMILES表示式與產率結合起來。
下面是具體程式碼:

!pip install pandas
import pandas as pd
from torch.utils.data import Dataset, DataLoader, Subset
from typing import List, Tuple
import re
import torch
import torch.nn as nn
import time
import torch.optim as optim
# 定義SMILES字串的分詞器類  
class Smiles_tokenizer():  
    def __init__(self, pad_token, regex, vocab_file, max_length):  
        # 初始化分詞器所需的引數  
        self.pad_token = pad_token  # 用於填充的token  
        self.regex = regex  # 用於匹配SMILES字串中有效部分的正規表示式  
        self.vocab_file = vocab_file  # 詞彙表檔案的路徑  
        self.max_length = max_length  # 序列的最大長度  
  
        # 載入詞彙表並構建詞彙表字典  
        with open(self.vocab_file, "r") as f:  
            lines = f.readlines()  
        lines = [line.strip("\n") for line in lines]  # 去除每行末尾的換行符  
        vocab_dic = {}  
        for index, token in enumerate(lines):  
            vocab_dic[token] = index  # 詞彙表中的token對映為其索引  
        self.vocab_dic = vocab_dic  
  
    def _regex_match(self, smiles):  
        # 使用正規表示式匹配SMILES字串,將每個字元或匹配項視為一個token  
        regex_string = r"(" + self.regex + r"|"  # 構造正規表示式,首先包含自定義的正規表示式部分  
        regex_string += r".)"  # 如果不匹配自定義部分,則匹配任意單個字元  
        prog = re.compile(regex_string)  # 編譯正規表示式  
  
        tokenised = []  
        for smi in smiles:  # 這裡假設smiles是單個字串的迭代(實際上可能應為字元迭代)  
            tokens = prog.findall(smi)  # 使用正規表示式查詢所有匹配項  
            if len(tokens) > self.max_length:  
                tokens = tokens[:self.max_length]  # 如果匹配項過多,則截斷  
            tokenised.append(tokens)  # 將匹配項列表新增到結果列表中  
        return tokenised  
      
    def tokenize(self, smiles):  
        # 對SMILES字串進行分詞處理  
        tokens = self._regex_match(smiles)  # 使用正規表示式進行分詞  
        # 為每個分詞後的序列新增開始和結束token  
        tokens = [["<CLS>"] + token + ["<SEP>"] for token in tokens]  
        tokens = self._pad_seqs(tokens, self.pad_token)  # 對序列進行填充  
        token_idx = self._pad_token_to_idx(tokens)   
        return tokens, token_idx  # 返回分詞後的序列和對應的索引序列(但後者需要實現_pad_token_to_idx方法)  
  
    def _pad_seqs(self, seqs, pad_token):  
        # 對序列進行填充,使得所有序列長度相同  
        pad_length = max([len(seq) for seq in seqs])  # 計算最長序列的長度  
        padded = [seq + ([pad_token] * (pad_length - len(seq))) for seq in seqs]  # 填充短序列  
        return padded
    def _pad_token_to_idx(self, tokens):  
        """  
        將分詞後的tokens轉換為對應的索引列表,並更新詞彙表以包含新發現的詞彙。  
  
        引數:  
            tokens (list of lists): 列表的列表,其中每個內部列表包含了一個SMILES字串分詞後的token。  
  
        過程:  
            1. 遍歷每個SMILES字串的分詞結果。  
            2. 對於每個token,檢查它是否已在詞彙表中。  
                - 如果在,則獲取其索引。  
                - 如果不在,則將其新增到詞彙表中,併為其分配一個新的索引。  
            3. 將每個SMILES字串的token索引列表收集起來。  
            4. 將新發現的詞彙寫入到檔案"../new_vocab_list.txt"中。  
  
        返回值:  
            idx_list (list of lists): 分詞後的SMILES字串對應的索引列表的列表。  
        """  
        idx_list = []  
        new_vocab = []  
        for token_list in tokens:  # 更清晰的變數名,表示每個SMILES字串的分詞結果  
            tokens_idx = []  
            for token in token_list:  # 遍歷當前SMILES字串的每個token  
                if token in self.vocab_dic.keys():  
                    tokens_idx.append(self.vocab_dic[token])  
                else:  
                    new_vocab.append(token)  
                    self.vocab_dic[token] = max(self.vocab_dic.values()) + 1  
                    tokens_idx.append(self.vocab_dic[token])  
            idx_list.append(tokens_idx)  
  
        with open("../new_vocab_list.txt", "a") as f:  
            for vocab in new_vocab:  # 更清晰的變數名  
                f.write(vocab)  
                f.write("\n")  
  
        return idx_list  
  
    def _save_vocab(self, vocab_path):  
        """  
        將當前詞彙表儲存到指定的檔案路徑。  
  
        引數:  
            vocab_path (str): 詞彙表將要儲存的檔案路徑。  
  
        過程:  
            1. 開啟(或建立)指定路徑的檔案。  
            2. 遍歷詞彙表的鍵(即詞彙),並將它們寫入到檔案中,每個詞彙後緊跟一個換行符。  
            3. 列印一條訊息,表明新的詞彙表已經被更新並儲存。  
        """  
        with open(vocab_path, "w") as f:  
            for vocab in self.vocab_dic.keys():  # 更清晰的變數名  
                f.write(vocab)  
                f.write("\n")  
        print("update new vocab!") 
def read_data(file_path, train=True):  
    """  
    從指定檔案路徑讀取資料,並準備用於訓練或預測的輸入和輸出資料。  
  
    引數:  
        file_path (str): 資料檔案的路徑。  
        train (bool): 指示是否讀取訓練資料。如果是訓練資料,則包含Yield列;否則,Yield列將被填充為0。  
  
    過程:  
        1. 使用pandas的read_csv函式讀取CSV檔案到DataFrame。  
        2. 從DataFrame中提取Reactant1, Reactant2, Product, Additive, Solvent列的資料,並轉換為列表。  
        3. 根據train引數決定是否讀取Yield列。如果是訓練模式,則讀取Yield;否則,為Reactant1的數量生成相同長度的0列表。  
        4. 遍歷Reactant1, Reactant2, Product, Additive, Solvent的列表(使用zip函式同時迭代),將Reactant1和Reactant2用'.'連線,然後與Product用'>>'連線,構建輸入資訊。   
        5. 將構建好的輸入資訊和對應的Yield(或0)組合成元組列表,作為輸出資料。  
  
    返回值:  
        output (list of tuples): 包含輸入資訊和對應Yield(或0)的元組列表。  
    """  
    df = pd.read_csv(file_path)  
    reactant1 = df["Reactant1"].tolist()  
    reactant2 = df["Reactant2"].tolist()  
    product = df["Product"].tolist()  
    additive = df["Additive"].tolist()  
    solvent = df["Solvent"].tolist()      
    if train:  
        react_yield = df["Yield"].tolist()  
    else:  
        react_yield = [0 for i in range(len(reactant1))]  
      
    input_data_list = []  
    for react1, react2, prod, addi, sol in zip(reactant1, reactant2, product, additive, solvent):  
        # 只將reactant1, reactant2和product拼接到一起,忽略了additive和solvent  
        input_info = ".".join([react1, react2])  
        input_info = ">".join([input_info, prod])  
        input_data_list.append(input_info)  
    output = [(react, y) for react, y in zip(input_data_list, react_yield)]  
  
    return output
# 定義資料集類,用於處理化學反應資料集  
class ReactionDataset(Dataset):  
    def __init__(self, data: List[Tuple[List[str], float]]):  
        """  
        初始化資料集類。  
  
        引數:  
            data (List[Tuple[List[str], float]]): 包含資料集的列表,每個元素是一個元組,元組的第一個元素是SMILES字串的列表(已處理為字串),  
                                                  第二個元素是該反應對應的產率(float型別)。  
        """  
        self.data = data  # 儲存資料集  
          
    def __len__(self):  
        """  
        返回資料集中的樣本數量。  
  
        返回:  
            int: 資料集中的樣本總數。  
        """  
        return len(self.data)  
  
    def __getitem__(self, idx):  
        """  
        根據索引獲取資料集中的單個樣本。  
  
        引數:  
            idx (int): 樣本的索引。  
  
        返回:  
            Tuple[List[str], float]: 一個元組,包含SMILES字串的列表和對應的產率。  
        """  
        return self.data[idx]  
  
# 定義自定義的collate_fn函式,用於在DataLoader中處理批次資料  
def collate_fn(batch):  
    """  
    將一批資料轉換為模型訓練所需的格式。  
  
    引數:  
        batch (List[Tuple[List[str], float]]): 一批資料,每個元素都是一個包含SMILES字串列表和產率的元組。  
  
    返回:  
        Tuple[torch.Tensor, torch.Tensor]: 處理後的SMILES字串的Tensor和產率的Tensor。  
    """  
    REGEX = r"\[[^\]]+]|Br?|Cl?|N|O|S|P|F|I|b|c|n|o|s|p|\(|\)|\.|=|#|-|\+|\\\\|\/|:|~|@|\?|>|\*|\$|\%[0-9]{2}|[0-9]"  
    # 初始化SMILES字串的tokenizer,這裡假設Smiles_tokenizer是一個自定義的tokenizer類  
    tokenizer = Smiles_tokenizer("<PAD>", REGEX, "../vocab_full.txt", 300)  
    smi_list = []  # 儲存SMILES字串的列表  
    yield_list = []  # 儲存產率的列表  
      
    # 遍歷批次中的每個樣本  
    for i in batch:  
        smi_list.append(i[0])  # 新增SMILES字串列表  
        yield_list.append(i[1])  # 新增產率  
      
    # 使用tokenizer將SMILES字串列表轉換為Tensor,注意這裡tokenizer.tokenize可能返回多個Tensor,這裡只取第一個[1](假設是處理後的SMILES)  
    tokenizer_batch = torch.tensor(tokenizer.tokenize(smi_list)[1])  
    # 將產率列表轉換為Tensor  
    yield_list = torch.tensor(yield_list)  
      
    # 返回處理後的SMILES Tensor和產率Tensor  
    return tokenizer_batch, yield_list
# 模型定義  
'''  
該模型直接採用了一個Transformer編碼器結構,用於處理輸入資料並輸出預測結果。  
'''  
class TransformerEncoderModel(nn.Module):  
    def __init__(self, input_dim, d_model, num_heads, fnn_dim, num_layers, dropout):  
        """  
        初始化Transformer編碼器模型。  
  
        引數:  
            input_dim (int): 輸入的詞彙表大小。  
            d_model (int): Transformer模型中的特徵維度。  
            num_heads (int): 多頭注意力機制中的頭數。  
            fnn_dim (int): 前饋神經網路中的維度。  
            num_layers (int): Transformer編碼器中的層數。  
            dropout (float): Dropout比率,用於防止過擬合。  
        """  
        super().__init__()  
        # 嵌入層,將輸入的索引轉換為d_model維的嵌入向量  
        self.embedding = nn.Embedding(input_dim, d_model)  
        # 層歸一化層,用於穩定訓練過程  
        self.layerNorm = nn.LayerNorm(d_model)  
        # Transformer編碼器層,包含多頭注意力機制和前饋神經網路  
        self.encoder_layer = nn.TransformerEncoderLayer(d_model=d_model,   
                                                        nhead=num_heads,   
                                                        dim_feedforward=fnn_dim,  
                                                        dropout=dropout,  
                                                        batch_first=True,  # 輸入的batch維度是第一維  
                                                        norm_first=True)   # 在多頭注意力和前饋神經網路之前進行層歸一化  
        # Transformer編碼器,由多個編碼器層堆疊而成  
        self.transformer_encoder = nn.TransformerEncoder(self.encoder_layer,   
                                                         num_layers=num_layers,  
                                                         norm=self.layerNorm)  # 注意:這裡的norm引數在PyTorch的較新版本中可能不被支援,應直接使用layerNorm層在模型的其他部分  
        # Dropout層,用於減少過擬合  
        self.dropout = nn.Dropout(dropout)  
        # 線性層組合,用於將Transformer的輸出轉換為最終的預測結果  
        self.lc = nn.Sequential(nn.Linear(d_model, 256),  # 第一層線性變換  
                                nn.Sigmoid(),            # Sigmoid啟用函式  
                                nn.Linear(256, 96),       # 第二層線性變換  
                                nn.Sigmoid(),            # Sigmoid啟用函式  
                                nn.Linear(96, 1))         # 輸出層,輸出預測結果  
  
    def forward(self, src):  
        """  
        模型的前向傳播過程。  
  
        引數:  
            src (Tensor): 輸入的源資料,形狀為[batch_size, src_len]。  
  
        返回:  
            Tensor: 模型的預測結果,形狀為[batch_size]。  
        """  
        # 將輸入透過嵌入層得到嵌入向量,並透過Dropout層  
        embedded = self.dropout(self.embedding(src))  
        # 將嵌入向量傳遞給Transformer編碼器  
        outputs = self.transformer_encoder(embedded)  
        # 選擇Transformer編碼器輸出的第一個時間步的隱藏狀態作為後續處理的輸入  
        # 這裡假設我們只關心序列的第一個元素(或者整個序列的某種聚合表示)  
        z = outputs[:,0,:]  
        # 透過線性層組合進行預測  
        outputs = self.lc(z)  
        # 去除最後一個維度(如果它是1的話),通常是因為輸出層設計為輸出單個值但保持了額外的維度  
        return outputs.squeeze(-1)
# 訓練函式  
def train():  
    ## 超引數設定  
    N = 10  # 使用的樣本數量,這裡直接設定為10,或者可以根據資料集大小動態調整,如 int(len(dataset) * 0.1)  
    INPUT_DIM = 292  # 輸入資料的維度,即源序列的長度  
    D_MODEL = 512  # Transformer模型中編碼器和解碼器嵌入的維度  
    NUM_HEADS = 4  # 多頭注意力機制中的頭數  
    FNN_DIM = 1024  # 前饋網路中的維度  
    NUM_LAYERS = 4  # Transformer模型中編碼器和解碼器的層數  
    DROPOUT = 0.2  # Dropout比例,用於防止過擬合  
    CLIP = 1  # 梯度裁剪的閾值,防止梯度爆炸  
    N_EPOCHS = 40  # 訓練的總週期數  
    LR = 1e-4  # 初始學習率  
      
    # 開始計時  
    start_time = time.time()  
      
    # 裝置選擇,優先使用GPU,如果不可用則使用CPU  
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')  
    # device = 'cpu'  # 註釋掉的程式碼,表示強制使用CPU  
      
    # 讀取資料  
    data = read_data("../dataset/round1_train_data.csv")  # 假設這是一個自定義的讀取資料的函式  
    dataset = ReactionDataset(data)  # 假設這是一個自定義的資料集類  
      
    # 實際上這裡只使用了前N個樣本進行訓練,但train_loader卻是對整個dataset進行取樣  
    # 這裡可能存在邏輯上的不一致,應該是想對subset_dataset使用DataLoader  
    subset_indices = list(range(N))  
    subset_dataset = Subset(dataset, subset_indices)  # 建立一個只包含前N個樣本的資料集  
    # 注意:下面的train_loader實際上是對整個dataset進行操作的,而不是subset_dataset  
    train_loader = DataLoader(dataset, batch_size=128, shuffle=True, collate_fn=collate_fn)  
      
    # 初始化模型  
    model = TransformerEncoderModel(INPUT_DIM, D_MODEL, NUM_HEADS, FNN_DIM, NUM_LAYERS, DROPOUT)  
    model = model.to(device)  # 將模型移動到選定的裝置上  
    model.train()  # 設定模型為訓練模式  
      
    # 初始化最佳化器和學習率排程器  
    optimizer = optim.AdamW(model.parameters(), lr=LR, weight_decay=0.01)  
    scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='min', factor=0.1, patience=10)  
    # 注意:這裡的scheduler使用方式可能需要調整,因為它依賴於驗證損失來觸發學習率調整  
    criterion = nn.MSELoss()  # 使用均方誤差作為損失函式  
      
    best_valid_loss = 10  # 假設的初始最佳驗證損失,用於比較  
      
    # 訓練迴圈  
    for epoch in range(N_EPOCHS):  
        epoch_loss = 0  # 初始化當前週期的損失總和  
        # adjust_learning_rate(optimizer, epoch, LR) # 動態調整學習率的程式碼被註釋掉了  
        for i, (src, y) in enumerate(train_loader):  
            # 將資料和標籤移動到選定的裝置上  
            src, y = src.to(device), y.to(device)  
              
            # 梯度清零  
            optimizer.zero_grad()  
              
            # 前向傳播  
            output = model(src)  
              
            # 計算損失  
            loss = criterion(output, y)  
              
            # 反向傳播  
            loss.backward()  
              
            # 梯度裁剪  
            torch.nn.utils.clip_grad_norm_(model.parameters(), CLIP)  
              
            # 更新引數  
            optimizer.step()  
              
            # 累加損失  
            epoch_loss += loss.detach().item()  
              
            # 每50步列印一次訓練損失  
            if i % 50 == 0:  
                print(f'Step: {i} | Train Loss: {epoch_loss:.4f}')  
              # 注意:這裡之前缺少了對scheduler的loss_in_a_epoch的傳遞  
    # 但由於我們是在訓練迴圈中,通常使用訓練損失來更新scheduler(儘管這取決於scheduler的具體型別)  
    # 這裡我們簡單地使用當前週期的平均訓練損失  
    scheduler.step(loss_in_a_epoch)  # 如果scheduler需要驗證損失,這裡需要調整  
    loss_in_a_epoch = epoch_loss / len(train_loader)  # 計算當前週期的平均訓練損失  
    print(f'Epoch: {epoch+1:02} | Train Loss: {loss_in_a_epoch:.3f}')  
      
    # 如果當前週期的平均訓練損失小於之前記錄的最佳驗證損失(儘管這裡實際上沒有驗證集)  
    # 我們假設這裡是一個簡化的例子,直接用訓練損失作為比較基準  
    if loss_in_a_epoch < best_valid_loss:  
        best_valid_loss = loss_in_a_epoch  
        # 儲存模型狀態字典到檔案  
        torch.save(model.state_dict(), '../model/transformer.pth')  
  
# 結束計時  
end_time = time.time()  
# 計算總執行時間(分鐘)  
elapsed_time_minute = (end_time - start_time) / 60  
# 列印總執行時間  
print(f"Total running time: {elapsed_time_minute:.2f} minutes")  
  
# 如果這段程式碼是作為指令碼直接執行的  
if __name__ == '__main__':  
    train()
# 生成結果檔案  
def predicit_and_make_submit_file(model_file, output_file):  
    # 定義模型引數  
    INPUT_DIM = 292  # 輸入序列的長度  
    D_MODEL = 512    # 模型的嵌入維度  
    NUM_HEADS = 4    # 自注意力機制中的頭數  
    FNN_DIM = 1024   # 前饋網路中的維度  
    NUM_LAYERS = 4   # 編碼器的層數  
    DROPOUT = 0.2    # Dropout比例  
  
    # 選擇裝置:如果可用則使用GPU,否則使用CPU  
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')  
  
    # 讀取測試資料  
    test_data = read_data("../dataset/round1_test_data.csv", train=False)  
    # 建立測試資料集  
    test_dataset = ReactionDataset(test_data)  
    # 建立測試資料載入器  
    test_loader = DataLoader(test_dataset, batch_size=128, shuffle=False, collate_fn=collate_fn)   
  
    # 初始化模型並移動到選定的裝置上  
    model = TransformerEncoderModel(INPUT_DIM, D_MODEL, NUM_HEADS, FNN_DIM, NUM_LAYERS, DROPOUT).to(device)  
    # 載入之前儲存的最佳模型狀態  
    model.load_state_dict(torch.load(model_file))  
    # 將模型設定為評估模式  
    model.eval()  
  
    # 初始化一個列表來儲存預測結果  
    output_list = []  
  
    # 遍歷測試資料載入器  
    for i, (src, y) in enumerate(test_loader):  
        # 將輸入資料移動到選定的裝置上  
        src = src.to(device)  
        # 關閉梯度計算,因為我們不需要在預測時更新模型引數  
        with torch.no_grad():  
            # 執行前向傳播以獲取預測輸出  
            output = model(src)  
            # 將預測輸出從張量轉換為列表,並新增到輸出列表中  
            output_list += output.detach().tolist()  
  
    # 初始化一個列表來構建最終的提交字串  
    ans_str_lst = ['rxnid,Yield']  # 提交檔案的第一行,包含列名  
  
    # 將預測結果轉換為字串格式,並新增到提交字串列表中  
    for idx, y in enumerate(output_list):  
        # 假設每個預測對應一個測試樣本,並格式化為'test{idx+1},{y:.4f}'  
        ans_str_lst.append(f'test{idx+1},{y:.4f}')  
  
    # 開啟輸出檔案,並將提交字串列表寫入檔案  
    with open(output_file, 'w') as fw:  
        fw.writelines('\n'.join(ans_str_lst))  
  
# 呼叫函式,傳入模型檔案路徑和輸出檔案路徑  
predicit_and_make_submit_file("../model/transformer.pth",  
                              "../output/result.txt")

相關文章