介紹
Francois Chollet寫的《Deep Learning with Python》一書讓我進入了深度學習的世界。從那時起我就愛上了Keras的風格。
Keras是我的第一個框架,然後是Tensorflow,接著進入PyTorch。
老實說,在Keras的模型訓練中,我很興奮這個進度條,真是太棒了。
那麼,為什麼不嘗試把Keras訓練模型的經驗帶到PyTorch呢?
這個問題讓我開始了工作,最後我用所有那些花哨的進度條重現了Keras的Dense層、卷積層和平坦層。
模型可以通過堆疊一層到另一層來創建,並通過簡單地調用fit方法進行訓練,該方法類似於Keras的工作方式。
Keras的工作方式如下:
#一層一層疊起來#採用輸入數據的形狀inputs = keras.Input(shape=(784,))l1 = layers.Dense(64, activation="relu")(inputs)l2 = layers.Dense(64, activation="relu")(l1)outputs = layers.Dense(10)(l2)model = keras.Model(inputs=inputs, outputs=outputs)#輸出模型摘要model.summary()#模型訓練和評估model.fit(x_train, y_train, epochs=2)model.evaluate(x_test, y_test)1.導入所需的庫
你可能不熟悉庫pkbar,它用於顯示類似Keras的進度條。
!pip install pkbarimport torchfrom torch import nnfrom torch import optimfrom torch.autograd import Variablefrom torchsummary import summary as summary_import pkbarimport warningswarnings.filterwarnings('ignore')2.輸入層和dense層
輸入層只是以數據的單一實例的形式被傳遞到神經網絡並返回它,對於全連接的網絡,它將類似於(1,784),對於卷積神經網絡,它將是圖像的尺寸(高度×寬度×通道)。
使用大寫字母來命名python函數是違反規則的,但是我們暫時忽略它(Keras原始碼的某些部分使用相同的約定)。
def Input(shape): Input.shape = shape return Input.shapedef get_conv_output(shape, inputs): bs = 1 data = Variable(torch.rand(bs, *shape)) output_feat = inputs(data) return output_feat.size(1)def same_pad(h_in, kernal, stride, dilation): return (stride*(h_in-1)-h_in+(dilation*(kernal-1))+1) / 2.0Dense類通過傳遞該層的輸出神經元數量和激活函數來初始化。調用Dense層時,前一層作為輸入傳遞。
現在我們有了關於前一層的信息。如果前一層是輸入層,則創建一個PyTorch線性層,其中輸入層返回的形狀和輸出神經元的數量作為Dense類初始化期間的參數。
如果前一層是Dense層,我們通過在Dense類中增加一個PyTorch線性層和一個激活層來擴展神經網絡。
如果前一層是卷積層或平坦層,我們將創建一個名為get_conv_output()的實用函數,通過卷積層和平坦層得到圖像的輸出形狀。此維度是必需的,因為如果不向in_features參數傳遞值,則無法在PyTorch中創建線性層。
函數的作用是將圖像形狀和卷積神經網絡模型作為輸入。然後,它創建一個與圖像形狀相同的虛擬張量,並將其傳遞給卷積網絡(具有平坦層),並返回從中輸出的數據的大小,該大小作為值傳遞給PyTorch線性層中的in_features參數。
class Dense(nn.Module): def __init__(self, outputs, activation): super().__init__() self.outputs = outputs self.activation = activation def __call__(self, inputs): self.inputs_size = 1 if type(inputs) == tuple: for i in range(len(inputs)): self.inputs_size *= inputs[i] self.layers = nn.Sequential( nn.Linear(self.inputs_size, self.outputs), self.activation ) return self.layers elif isinstance(inputs[-2], nn.Linear): self.inputs = inputs self.layers = list(self.inputs) self.layers.extend([nn.Linear(self.layers[-2].out_features, self.outputs), self.activation]) self.layers = nn.Sequential(*self.layers) return self.layers else: self.inputs = inputs self.layers = list(self.inputs) self.layers.extend([nn.Linear(get_conv_output(Input.shape, self.inputs), self.outputs), self.activation]) self.layers = nn.Sequential(*self.layers) return self.layers3.平坦層
為了創建一個平坦層,我們將創建一個名為FlattenedLayer的自定義層類,它接受張量作為輸入,並在前向傳播期間返回張量的平坦版本。
我們將創建另一個名為flatten的類,當調用這個層時,前面的層作為輸入傳遞,然後flatten類通過在前面的層上添加我們自定義創建的FlattenedLayer類來擴展網絡。
因此,所有到達平坦層的數據都是使用我們自定義創建的平坦層進行平坦的。
class FlattenedLayer(nn.Module): def __init__(self): super().__init__() pass def forward(self, input): self.inputs = input.view(input.size(0), -1) return self.inputsclass Flatten(): def __init__(self): pass def __call__(self, inputs): self.inputs = inputs self.layers = list(self.inputs) self.layers.extend([FlattenedLayer()]) self.layers = nn.Sequential(*self.layers) return self.layers4.卷積層
我們將通過傳入濾波器數量、內核大小、步長、填充、膨脹和激活函數來初始化Conv2d層。
現在,當調用Conv2d層時,前面的層被傳遞給它,如果前一層是Input layer,則是一個Pytorch conv2d層,其中提供了濾波器數量、內核大小、步長、填充,擴張和激活函數被創建,其中in_channels的值取自輸入形狀中的通道數。
如果前一層是卷積層,則通過添加一個PyTorch Conv2d層和激活函數來擴展前一層,激活函數的值取自前一層的out_channels 。
在填充的情況下,如果用戶需要保留從該層傳出的數據的維度,則可以將padding的值指定為「same」,而不是整數。
如果padding的值被指定為「same」,那麼將使用一個名為same_pad()的實用函數來獲取padding的值,以保留給定輸入大小、內核大小、步長和膨脹的維度。
可以使用前面討論的get_conv_output()實用程序函數獲得輸入大小。
class Conv2d(nn.Module): def __init__(self, filters, kernel_size, strides, padding, dilation, activation): super().__init__() self.filters = filters self.kernel = kernel_size self.strides = strides self.padding = padding self.dilation = dilation self.activation = activation def __call__(self, inputs): if type(inputs) == tuple: self.inputs_size = inputs if self.padding == 'same': self.padding = int(same_pad(self.inputs_size[-2], self.kernel, self.strides, self.dilation)) else: self.padding = self.padding self.layers = nn.Sequential( nn.Conv2d(self.inputs_size[-3], self.filters, self.kernel, self.strides, self.padding, self.dilation), self.activation ) return self.layers else: if self.padding == 'same': self.padding = int(same_pad(get_conv_output(Input.shape, inputs), self.kernel, self.strides, self.dilation)) else: self.padding = self.padding self.inputs = inputs self.layers = list(self.inputs) self.layers.extend( [nn.Conv2d(self.layers[-2].out_channels, self.filters, self.kernel, self.strides, self.padding, self.dilation), self.activation] ) self.layers = nn.Sequential(*self.layers) return self.layers5.模型類
在構建了模型的體系結構之後,通過傳入輸入層和輸出層來初始化模型類。但是我已經給出了一個額外的參數,名為device,它在Keras中不存在,這個參數接受值為'CPU'或'CUDA',它將把整個模型移動到指定的設備。
model類的parameters方法用於返回要給PyTorch優化器的模型參數。
model類有一個名為compile的方法,它接受訓練模型所需的優化器和丟失函數。模型類的摘要方法是藉助torch的summary庫顯示所創建模型的摘要。
採用擬合方法對模型進行訓練,該方法以輸入特徵集、目標數據集和epoch數為參數。它顯示由損失函數計算的損失和使用pkbar庫的訓練進度。
評估會計算驗證數據集的損失和精度。
當使用PyTorch數據加載程序加載數據時,將使用fit_generator、evaluate_generator 和predict_generator 。fit_generator 以訓練集數據加載器和epoch作為參數。evaluate_generator和predict_generator分別使用驗證集數據加載器和測試數據加載器來衡量模型對未查看數據的執行情況。
class Model(): def __init__(self, inputs, outputs, device): self.input_size = inputs self.device = device self.model = outputs.to(self.device) def parameters(self): return self.model.parameters() def compile(self, optimizer, loss): self.opt = optimizer self.criterion = loss def summary(self): summary_(self.model, self.input_size, device=self.device) print("Device Type:", self.device) def fit(self, data_x, data_y, epochs): self.model.train() for epoch in range(epochs): print("Epoch {}/{}".format(epoch+1, epochs)) progress = pkbar.Kbar(target=len(data_x), width=25) for i, (data, target) in enumerate(zip(data_x, data_y)): self.opt.zero_grad() train_out = self.model(data.to(self.device)) loss = self.criterion(train_out, target.to(self.device)) loss.backward() self.opt.step() progress.update(i, values=[("loss: ", loss.item())]) progress.add(1) def evaluate(self, test_x, test_y): self.model.eval() correct, loss = 0.0, 0.0 progress = pkbar.Kbar(target=len(test_x), width=25) for i, (data, target) in enumerate(zip(test_x, test_y)): out = self.model(data.to(self.device)) loss += self.criterion(out, target.to(self.device)) correct += ((torch.max(out, 1)[1]) == target.to(self.device)).sum() progress.update(i, values=[("loss", loss.item()/len(test_x)), ("acc", (correct/len(test_x)).item())]) progress.add(1) def fit_generator(self, generator, epochs): self.model.train() for epoch in range(epochs): print("Epoch {}/{}".format(epoch+1, epochs)) progress = pkbar.Kbar(target=len(generator), width=25) for i, (data, target) in enumerate(generator): self.opt.zero_grad() train_out = self.model(data.to(self.device)) loss = self.criterion(train_out.squeeze(), target.to(self.device)) loss.backward() self.opt.step() progress.update(i, values=[("loss: ", loss.item())]) progress.add(1) def evaluate_generator(self, generator): self.model.eval() correct, loss = 0.0, 0.0 progress = pkbar.Kbar(target=len(generator), width=25) for i, (data, target) in enumerate(generator): out = self.model(data.to(self.device)) loss += self.criterion(out.squeeze(), target.to(self.device)) correct += (torch.max(out.squeeze(), 1)[1] == target.to(self.device)).sum() progress.update(i, values=[("test_acc", (correct/len(generator)).item()), ("test_loss", loss.item()/len(generator))]) progress.add(1) def predict_generator(self, generator): self.model.train() out = [] for i, (data, labels) in enumerate(generator): out.append(self.model(data.to(self.device))) return out結尾
我用Dense層和卷積神經網絡在CIFAR100、CIFAR10和MNIST數據集上測試了代碼。它工作得很好,但還有很大的改進空間。
這是一個有趣的項目,我已經工作了3-4天,它真的突破了我用PyTorch編程的極限。
你可以在這裡查看完整的代碼,並在上面提到的數據集上進行訓練,或者你可以自由地調整代碼以適合你在colab中的喜好:https://colab.research.google.com/github/bipinKrishnan/torchkeras/blob/master/functional_api_v1.ipynb