Learning both Weights and Connections for Efficient Neural Networks 論文pytorch復現
這是論文中主要的步驟,因此我們復現的時候也主要是利用這個思想。
代碼編寫需要兩個主要部分,首先原來神經網絡的訓練,然後就是神經網絡的裁剪。我這次實驗主要是使用論文中說的Lenet-300-100網絡來進行測試。
網絡的定義以及初次訓練
##############先導入需要的包###############################
import torch
import torch.nn as nn
from torchvision import datasets, transforms
import torch.optim as optim
import numpy as np
import matplotlib.pyplot as plt
import torch.nn.functional as F
##############導入手寫數字體數據###############################
batch_size = 128
mnist_data = datasets.MNIST('./mnist_data', train=True, download=True,
transform=transforms.Compose([
transforms.ToTensor(),
]))
dataloader = torch.utils.data.DataLoader(dataset=mnist_data,
batch_size=batch_size,
shuffle=True,num_workers = 20)
##############顯示手寫數字體數據###############################
for i in range(10):
plt.figure()
plt.imshow(next(iter(dataloader))[0][0][0],cmap="gray")
##############定義網絡Lenet-300-100網絡###############################
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.fc1 = nn.Linear(28*28, 300)
self.fc2 = nn.Linear(300, 100)
self.fc3 = nn.Linear(100, 10)
def forward(self, x):
x = x.view(-1,28*28)
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = self.fc3(x)
return F.log_softmax(x, dim=1)
def train(model, device, train_loader, optimizer, epoch, log_interval=100):
model.train()
for batch_idx, (data, target) in enumerate(train_loader):
data, target = data.to(device), target.to(device)
optimizer.zero_grad()
output = model(data)
loss = F.nll_loss(output, target)
loss.backward()
optimizer.step()
if batch_idx % log_interval == 0:
print("Train Epoch: {} [{}/{} ({:0f}%)]\tLoss: {:.6f}".format(
epoch, batch_idx * len(data), len(train_loader.dataset),
100. * batch_idx / len(train_loader), loss.item()
))
def test(model, device, test_loader):
model.eval()
test_loss = 0
correct = 0
with torch.no_grad():
for data, target in test_loader:
data, target = data.to(device), target.to(device)
output = model(data)
test_loss += F.nll_loss(output, target, reduction='sum').item() # sum up batch loss
pred = output.argmax(dim=1, keepdim=True) # get the index of the max log-probability
correct += pred.eq(target.view_as(pred)).sum().item()
test_loss /= len(test_loader.dataset)
print('\nTest set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format(
test_loss, correct, len(test_loader.dataset),
100. * correct / len(test_loader.dataset)))
##############初次訓練###############################
lr = 0.01
momentum = 0.25
torch.manual_seed(53113)
batch_size = test_batch_size = 128
kwargs = {'num_workers': 40, 'pin_memory': True} if use_cuda else {}
train_loader = torch.utils.data.DataLoader(
datasets.MNIST('./mnist_data', train=True, download=True,
transform=transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,))
])),
batch_size=batch_size, shuffle=True, **kwargs)
test_loader = torch.utils.data.DataLoader(
datasets.MNIST('./mnist_data', train=False, transform=transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,))
])),
batch_size=test_batch_size, shuffle=True, **kwargs)
model = Net().to(device)
optimizer = optim.SGD(model.parameters(), lr=lr, momentum=momentum)
epochs = 20
for epoch in range(1, epochs + 1):
train(model, device, train_loader, optimizer, epoch)
test(model, device, test_loader)
訓練好網絡信息的顯示
#####################獲取一張圖像信息#################################
image = next(iter(dataloader))[0][0][0]
output = model.forward(image)
pred = output.argmax(dim=1, keepdim=True)
print(pred) #預測值
#####################顯示這張圖像#################################
plt.figure()
plt.imshow(image,cmap="gray")
plt.show()
#####################圖像預處理#################################
image = image.reshape(-1,28*28)
image = image.data.numpy()
image_count = image.copy()
#####################計算圖像非零點個數#################################
image_count[image_count != 0] = 1
count = np.sum(image_count)
print(count)
#####################轉化到numpy對數據進行處理#################################
fc1 = model.fc1.weight.data.cpu().numpy()
fc2 = model.fc2.weight.data.cpu().numpy()
fc3 = model.fc3.weight.data.cpu().numpy()
hidden1 = image.dot(fc1.T)
hidden1 = np.maximum(0, hidden1) #激活
hidden2 = hidden1.dot(fc2.T)
hidden2 = np.maximum(0, hidden2)
hidden3 = hidden2.dot(fc3.T)
#####################第一層神經元激活的個數#################################
hidden1_count = hidden1.copy()
hidden1_count[hidden1_count > 0] = 1
count = np.sum(hidden1_count)
print(count)
#####################第二層神經元激活的個數#################################
hidden2_count = hidden2.copy()
hidden2_count[hidden2_count > 0] = 1
count = np.sum(hidden2_count)
print(count)
#####################輸出預測結果#################################
out = np.exp(hidden3)
out = out / np.sum(out)
print(np.argmax(out))
#####################第一層神經元參數信息的圖像深度顯示,從這裏可以觀測出權重的變化#################################
fc1_plt = np.abs(fc1)
print("min",np.min(fc1_plt),"max",np.max(fc1_plt))
plt.figure(figsize=(50,50))
im = plt.imshow(fc1_plt, vmin = np.min(fc1_plt), vmax = np.max(fc1_plt) ,cmap = 'seismic')
plt.show()
剪枝操作
##########################函數定義###########################################
def expand_model(model, layers=torch.Tensor()):
for layer in model.children():
layers = torch.cat((layers.view(-1), layer.weight.view(-1))) #將所有的參數拼接在一起
return layers
def calculate_threshold(model, rate): #求取所有參數的閾值所在的數值大小
empty = torch.Tensor()
if torch.cuda.is_available():
empty = empty.cuda()
pre_abs = expand_model(model, empty) #獲取所有的參數爲一行
weights = torch.abs(pre_abs) #求絕對值
return np.percentile(weights.detach().cpu().numpy(), rate)
def prune(model, threshold):
model.fc1.weight.data = torch.mul(torch.gt(torch.abs(model.fc1.weight.data), threshold), model.fc1.weight.data)
model.fc2.weight.data = torch.mul(torch.gt(torch.abs(model.fc2.weight.data), threshold), model.fc2.weight.data)
model.fc3.weight.data = torch.mul(torch.gt(torch.abs(model.fc3.weight.data), threshold), model.fc3.weight.data)
def retrain(model, device, train_loader, test_loader, epochs, lr, momentum):
optimizer = optim.SGD(model.parameters(), lr=lr, momentum=momentum)
for epoch in range(1, epochs + 1):
train(model, device, train_loader, optimizer, epoch)
test(model, device, test_loader)
##########################按百分比計算剪枝的閾值###########################################
threshold = calculate_threshold(model, 96)
print(threshold)
##########################進行剪枝操作並計算測試正確率###########################################
prune(model, threshold)
test(model, device, test_loader)
##########################剪枝操作後進行再次訓練###########################################
retrain(model, device, train_loader ,test_loader, 20, 0.001, 0.25)
權重柱狀圖繪製
# 提取繪製的數據
def paraCount(layers, location, index):
values = []
for value in layers:
if location[index] == 0:
values.append(value)
index += 1
values = np.array(values)
return values
###########################繪製剪枝後的整體參數分佈############################
layers = expand_model(model)
layers = layers.detach().cpu().numpy()
values = paraCount(layers, location, 0)
plt.figure(figsize=(20,5))
plt.hist(values, bins = 1000)
plt.show()
###########################繪製第一層網絡參數分佈############################
fc1_hist = fc1.reshape(-1)
fc1_hist = paraCount(fc1_hist, location, 0)
plt.figure(figsize=(20,5))
plt.hist(fc1_hist, bins = 300)
plt.show()
###########################繪製第二層網絡參數分佈############################
fc2_hist = fc2.reshape(-1)
fc2_hist = paraCount(fc2_hist, location, fc1.shape[0] * fc1.shape[1])
plt.figure(figsize=(20,5))
plt.hist(fc2_hist, bins = 100)
plt.show()
###########################繪製第三層網絡參數分佈############################
fc3_hist = fc3.reshape(-1)
fc3_hist = paraCount(fc3_hist,location, fc1.shape[0] * fc1.shape[1] + fc2.shape[0] * fc2.shape[1])
plt.figure(figsize=(20,5))
plt.hist(fc3_hist, bins = 100)
plt.show()
這個剪枝操作需要重複進行,這個全連接的神經網絡,我可以剪枝掉98%後,正確率還是會有90%,而且經過多次剪枝操作後,網絡大部分的參數接近與零,只有重要的參數纔會起作用。
jupyter下實現的代碼,需要的可以去下載: link.
實驗
下面就是基於以上的代碼,使用手寫數字體的數據集進行的實驗,我會記錄實驗過程中的數據變化過程。
初次訓練網絡
神經元激活
從上圖可以看出,經過20次的迭代,網絡的正確率已經到達了97%
對於數字4的圖像進行識別,圖像的非零點數是75個,第一層神經元激活個數169,第二層神經元激活個數70,識別結果是4正確
層數 | 個數 |
---|---|
fc1 | 169 |
fc2 | 70 |
fc3 | 結果是4 |
第一層權重的深度分佈圖
下面是初次訓練第一層權重的深度分佈圖,從這裏可以明顯的看到了28的分割輪廓了,兩端的參數較小,不太重要。
權重柱狀圖
下圖是全部參數的柱狀圖、第一層參數的柱狀圖、第二層參數的柱狀圖、第三層參數的柱狀圖,可以看出參數都基本集中的0附近,成中心分佈的樣子。
剪枝10%參數
計算出來的閾值是:0.0038155303103849293
剪枝後的正確率並沒有明顯的變化,仍舊是97%
然後進行再訓練
比初次訓練的正確個數還多了,說不定這個可以增加泛化能力。
神經元激活
層數 | 個數 |
---|---|
fc1 | 167 |
fc2 | 71 |
fc3 | 結果是4 |
第一層權重的深度分佈圖
仔細對比可以發現,亮的地方變的更亮了一點。
權重柱狀圖
剪枝20%參數
計算出來的閾值是:0.007650174759328366
剪枝後的正確率並沒有明顯的變化,仍舊是97%
然後進行再訓練
比上訓練的正確個數還多了,又增加泛化能力。
神經元激活
層數 | 個數 |
---|---|
fc1 | 172 |
fc2 | 73 |
fc3 | 結果是4 |
第一層權重的深度分佈圖
仔細對比可以發現,這次好像圖像暗的地方增多了
權重柱狀圖
剪枝30%參數
計算出來的閾值是:0.01150485947728157
剪枝後的正確率並沒有明顯的變化,仍舊是97%
然後進行再訓練
比上訓練的正確個數還多了,又增加泛化能力。
神經元激活
層數 | 個數 |
---|---|
fc1 | 169 |
fc2 | 74 |
fc3 | 結果是4 |
第一層權重的深度分佈圖
仔細對比可以發現,這次好像圖像暗的地方更暗了
權重柱狀圖
剪枝40%參數
計算出來的閾值是:0.01536105088889599
剪枝後的正確率並沒有明顯的變化,仍舊是97%
然後進行再訓練
比上訓練的正確個數還多了,又增加泛化能力。
神經元激活
層數 | 個數 |
---|---|
fc1 | 167 |
fc2 | 73 |
fc3 | 結果是4 |
第一層權重的深度分佈圖
仔細對比可以發現,這次好像圖像暗的地方更暗了
權重柱狀圖
剪枝50%參數
計算出來的閾值是:0.019335072487592697
剪枝後的正確率並沒有明顯的變化,仍舊是97%
然後進行再訓練
比上訓練的正確個數還多了,又增加泛化能力。
神經元激活
層數 | 個數 |
---|---|
fc1 | 171 |
fc2 | 73 |
fc3 | 結果是4 |
第一層權重的深度分佈圖
仔細對比可以發現,這次好像圖像暗的地方更暗了
權重柱狀圖
剪枝60%參數
計算出來的閾值是:0.023352954909205435
剪枝後的正確率並沒有明顯的變化,仍舊是97%
然後進行再訓練
比上訓練的正確個數還多了,又增加泛化能力。
神經元激活
層數 | 個數 |
---|---|
fc1 | 176 |
fc2 | 74 |
fc3 | 結果是4 |
第一層權重的深度分佈圖
仔細對比可以發現,這次好像圖像暗的地方更暗了
權重柱狀圖
剪枝70%參數
計算出來的閾值是:0.02758437413722277
剪枝後的正確率並沒有明顯的變化,仍舊是97%
然後進行再訓練
比上訓練的正確個數還多了,又增加泛化能力。
神經元激活
層數 | 個數 |
---|---|
fc1 | 174 |
fc2 | 73 |
fc3 | 結果是4 |
第一層權重的深度分佈圖
仔細對比可以發現,這次好像圖像暗的地方更暗了
權重柱狀圖
剪枝80%參數
計算出來的閾值是:0.03225095123052597
剪枝後的正確率並沒有明顯的變化,仍舊是97%
然後進行再訓練
和上次訓練的正確個數差不多,網絡性能沒有增加了
神經元激活
層數 | 個數 |
---|---|
fc1 | 181 |
fc2 | 77 |
fc3 | 結果是4 |
第一層權重的深度分佈圖
仔細對比可以發現,這次好像圖像暗的地方更暗了
權重柱狀圖
剪枝90%參數
計算出來的閾值是:0.039930999279022224
剪枝後的正確率發生了變化,下降到了93%
然後進行再訓練
和上次訓練的正確個數差不多,網絡性能沒有增加了
神經元激活
層數 | 個數 |
---|---|
fc1 | 184 |
fc2 | 78 |
fc3 | 結果是4 |
第一層權重的深度分佈圖
仔細對比可以發現,這次好像圖像暗的地方更暗了
權重柱狀圖
剪枝95%參數
計算出來的閾值是:0.05171532221138477
剪枝後的正確率發生了變化,下降到了85%
然後進行再訓練
和上次訓練的正確個數差不多,網絡性能沒有增加了
神經元激活
層數 | 個數 |
---|---|
fc1 | 189 |
fc2 | 84 |
fc3 | 結果是4 |
第一層權重的深度分佈圖
仔細對比可以發現,這次好像圖像暗的地方更暗了
權重柱狀圖
剪枝98%參數
計算出來的閾值是:0.07121909260749816
剪枝後的正確率降到了很低,下降到了60%
然後進行再訓練
和上次訓練的正確個數差不多,網絡性還是能夠恢復的
神經元激活
層數 | 個數 |
---|---|
fc1 | 174 |
fc2 | 76 |
fc3 | 結果是4 |
第一層權重的深度分佈圖
仔細對比可以發現,這次好像圖像暗的地方更暗了
權重柱狀圖
剪枝99%參數
計算出來的閾值是:0.091620362251997
剪枝後的正確率降到了很低,下降到了46%
然後進行再訓練
和上次訓練的正確個數下降一個百分點,網絡性還是能夠恢復的,但是恢復的性能略有下降
神經元激活
層數 | 個數 |
---|---|
fc1 | 169 |
fc2 | 74 |
fc3 | 結果是4 |
第一層權重的深度分佈圖
仔細對比可以發現,這次好像圖像暗的地方更暗了
權重柱狀圖
剪枝99%參數
計算出來的閾值是:0.04240277148829452
剪枝後的正確率降到了很低,下降到了50%
然後進行再訓練
和上次訓練的正確個數下降一個百分點,網絡性還是能夠恢復的,但是恢復的性能略有下降
神經元激活
層數 | 個數 |
---|---|
fc1 | 167 |
fc2 | 75 |
fc3 | 結果是4 |
第一層權重的深度分佈圖
仔細對比可以發現,這次好像圖像暗的地方更暗了
權重柱狀圖
剪枝99%參數
計算出來的閾值是:0.04132488820748184
剪枝後的正確率降到了很低,下降到了50%
然後進行再訓練
和上次訓練的正確個數下降一個百分點,網絡性還是能夠恢復的,但是恢復的性能略有下降
神經元激活
層數 | 個數 |
---|---|
fc1 | 167 |
fc2 | 77 |
fc3 | 結果是4 |
第一層權重的深度分佈圖
仔細對比可以發現,這次好像圖像暗的地方更暗了
權重柱狀圖
剪枝99%參數
計算出來的閾值是:0.04077488094611925
剪枝後的正確率降到了很低,下降到了50%
然後進行再訓練
和上次訓練的正確個數下降一個百分點,網絡性還是能夠恢復的,但是恢復的性能略有下降
神經元激活
層數 | 個數 |
---|---|
fc1 | 167 |
fc2 | 77 |
fc3 | 結果是4 |
第一層權重的深度分佈圖
仔細對比可以發現,這次好像圖像暗的地方更暗了
權重柱狀圖
剪枝99%參數
經過非常多次剪枝99%然後參數再訓練,整個網絡的性能還是會慢慢提升的
計算出來的閾值是:0.025321735092738397
剪枝後的正確率降到了很低,下降到了65%,但是比剛開始還是有明顯的提高
然後進行再訓練
和上次訓練的正確個數下降一個百分點,網絡性還是能夠恢復的,還是快恢復到了巔峯時期
然後剪枝80%
計算出來的閾值是:0.0012542949290946129
層數 | 個數 |
---|---|
fc1 | 124 |
fc2 | 69 |
fc3 | 結果是4 |
然後剪枝90%
計算出來的閾值是:0.0027993932599201803
層數 | 個數 |
---|---|
fc1 | 101 |
fc2 | 66 |
fc3 | 結果是4 |
然後剪枝95%
計算出來的閾值是:0.005025399965234099
層數 | 個數 |
---|---|
fc1 | 78 |
fc2 | 64 |
fc3 | 結果是4 |
然後剪枝99%
計算出來的閾值是:0.02462294178083847
層數 | 個數 |
---|---|
fc1 | 25 |
fc2 | 56 |
fc3 | 結果是1 |
通過上面的一系列數據,其實可以發現,網絡中90的參數可以刪掉,幾乎不會影響準確性。95%的刪掉,準確性還是可以接受。但是相對於參數的減少,在實際運算過程中,激活神經元的減少數量其實沒有那麼明顯,當刪除95% 的參數時,纔會減少一半左右的神經元,當刪除99%的參數時,神經元能夠大幅度減少,但是可以發現現在的預測準確性已經無法接受。
同時,也可以發現,當前可以刪除的神經元是在第一層全連接層,而第二層全連接層明顯變化沒有第一層劇烈。
思考:
神經網絡的結構是每遞進一層深度,所獲得的信息就會更加抽象,說明在深層處抽象的特徵多餘的較少。
第一層權重的深度分佈圖
仔細對比可以發現,圖片裏面亮起來的點已經非常少了,基本上就是亮的和不亮的了。
權重柱狀圖
99%裁剪的情況下,一共使用了2662參數。
第一層一共使用了379個參數,此時和剛開始2828300=235200,減少了620倍,在淺層的多餘參數這麼多的嘛。而且,也可以看到,參數大量集中到了0.2左右的中心分佈,偏離了原來以0爲中心分佈的情況。
第二層一共使用了1654個參數,此時和剛開始100*300=30000,減少了18倍。參數幾乎還是以零爲中心在分佈。
第三層一共使用了629個參數,此時和剛開始100*10=1000,減少了1.59倍。可見參數其實減少的非常少。
思考與提問
從上面的分析,可以發現,越深層的參數和神經元越重要,從參數的大小和被裁剪的多少都可以明顯的看出。這裏我們是按照所有層的參數放在一起按百分比進行裁剪,如果是按照每一層自己的百分比進行裁剪,是不是整個訓練精度就會下降很多,明顯不如這一個?下面可以做一個實驗測試一下。
還有就是,現在我們第一層的神經元是300個,底層每次要激活170個左右,大概是在50%的激活率,如果我們把第一層的神經元個數增加到600個,那麼每次激活的個數還是50%左右還是170嗎?,如果我們進行同樣的剪枝操作之後激活的個數或者百分比有什麼變化?