雖然Yann LeCun在上個世紀就提出了卷積神經網絡LeNet,並使用LeNet進行圖像分類,但卷積神經網絡並沒有就此飛速發展。在LeNet提出後的將近20年裡,神經網絡一度被其他機器學習方法超越,如支持向量機。卷積神經網絡在當時未能快速發展主要受限於:
1. 缺少數據深度學習需要大量的有標籤的數據才能表現得比其他經典方法更好。限於早期計算機有限的存儲和90年代有限的研究預算,大部分研究只基於小的公開數據集。例如,不少研究論文基於加州大學歐文分校(UCI)提供的若干個公開數據集,其中許多數據集只有幾百至幾千張圖像。這一狀況在2010年前後興起的大數據浪潮中得到改善。特別是,李飛飛主導的ImageNet數據集的構建。ImageNet數據集包含了1,000大類物體,每類有多達數千張不同的圖像,數據總量達到了上百GB。這一規模是當時其他公開數據集無法與之相提並論的。此外,社區每年都舉辦一個挑戰賽,名為ImageNet Large-Scale Visual Recognition Challenge (ILSVRC) ,參賽選手需要基於ImageNet數據集,優化計算機視覺相關任務。可以說,ImageNet數據集推動了計算機視覺和機器學習研究進入新的階段。
2. 缺少硬體深度學習對計算資源要求很高。早期的硬體計算能力有限,這使訓練較複雜的神經網絡變得很困難。然而,通用GPU(General Purpose GPU,GPGPU)的到來改變了這一格局。很久以來,GPU都是為圖像處理和計算機遊戲設計的,尤其是針對大吞吐量的矩陣和向量乘法。值得慶幸的是,這其中的數學表達與深度網絡中的卷積層的表達類似。通用GPU這個概念在2001年開始興起,湧現出諸如CUDA和OpenCL之類的編程框架。CUDA編程接口上手難度沒那麼大,科研工作者可以使用CUDA在英偉達的GPU上加速自己的科學計算任務。一些計算密集型的任務在2010年左右開始被遷移到英偉達的GPU上。
人們普遍認為,當前這波人工智慧熱潮起源於2012年。當年,Alex Krizhevsky使用英偉達GPU成功訓練出了深度卷積神經網絡AlexNet,並憑藉該網絡在ImageNet挑戰賽上奪得冠軍,大幅提升圖像分類的準確度。當時,大數據的存儲和計算幾乎不再是瓶頸,AlexNet的提出也讓學術圈和工業界認識到深度神經網絡的驚人表現。
AlexNet網絡結構AlexNet與LeNet的設計理念非常相似,但也有顯著的區別。
LeNet與AlexNet第一,與相對較小的LeNet相比,AlexNet包含8層變換,其中有5層卷積和2層全連接隱藏層,以及1個全連接輸出層。下面我們來詳細描述這些層的設計。
AlexNet第一層中的卷積窗口形狀是11 × 11。因為ImageNet中絕大多數圖像的高和寬均比MNIST圖像的高和寬大10倍以上,ImageNet圖像的物體佔用更多的像素,所以需要更大的卷積窗口來捕獲物體。第二層中的卷積窗口形狀減小到5 × 5,之後全採用3 × 3。此外,第一、第二和第五個卷積層之後都使用了窗口形狀為3 × 3、步幅為2的最大池化層。而且,AlexNet使用的卷積通道數也大於LeNet中的卷積通道數數十倍。
緊接著最後一個卷積層的是兩個輸出個數為4096的全連接層。這兩個巨大的全連接層帶來將近1 GB的模型參數。由於早期顯存的限制,最早的AlexNet使用雙數據流的設計使一個GPU只需要處理一半模型。幸運的是,顯存在過去幾年得到了長足的發展,因此通常我們不再需要這樣的特別設計了。
第二,AlexNet將sigmoid激活函數改成了更加簡單的ReLU激活函數。一方面,ReLU激活函數的計算更簡單,例如它並沒有sigmoid激活函數中的求冪運算。另一方面,ReLU激活函數在不同的參數初始化方法下使模型更容易訓練。這是由於當sigmoid激活函數輸出極接近0或1時,這些區域的梯度幾乎為0,從而造成反向傳播無法繼續更新部分模型參數;而ReLU激活函數在正區間的梯度恆為1。因此,若模型參數初始化不當,sigmoid函數可能在正區間得到幾乎為0的梯度,從而令模型無法得到有效訓練。
第三,AlexNet通過丟棄法(Dropout)來控制全連接層的模型複雜度,避免過擬合。而LeNet並沒有使用丟棄法。
第四,AlexNet引入了大量的圖像增廣,如翻轉、裁剪和顏色變化,從而進一步擴大數據集來緩解過擬合。
下面是一個使用PyTorch實現的稍微簡化過的AlexNet。這個網絡假設使用1 × 224 × 224的輸入,即輸入只有一個通道,比如Fashion-MNIST這樣的黑白單顏色的數據集。
class AlexNet(nn.Module):
def __init__(self):
super(AlexNet, self).__init__()
# convolution layer will change input shape into: floor((input_shape - kernel_size + padding + stride) / stride)
# input shape: 1 * 224 * 224
# convolution part
self.conv = nn.Sequential(
# conv layer 1
# floor((224 - 11 + 2 + 4) / 4) = floor(54.75) = 54
# conv: 1 * 224 * 224 -> 96 * 54 * 54
nn.Conv2d(in_channels=1, out_channels=96, kernel_size=11, stride=4, padding=1), nn.ReLU(),
# floor((54 - 3 + 2) / 2) = floor(26.5) = 26
# 96 * 54 * 54 -> 96 * 26 * 26
nn.MaxPool2d(kernel_size=3, stride=2),
# conv layer 2: decrease kernel size, add padding to keep input and output size same, increase channel number
# floor((26 - 5 + 4 + 1) / 1) = 26
# 96 * 26 * 26 -> 256 * 26 * 26
nn.Conv2d(in_channels=96, out_channels=256, kernel_size=5, stride=1, padding=2), nn.ReLU(),
# floor((26 - 3 + 2) / 2) = 12
# 256 * 26 * 26 -> 256 * 12 * 12
nn.MaxPool2d(kernel_size=3, stride=2),
# 3 consecutive conv layer, smaller kernel size
# floor((12 - 3 + 2 + 1) / 1) = 12
# 256 * 12 * 12 -> 384 * 12 * 12
nn.Conv2d(in_channels=256, out_channels=384, kernel_size=3, stride=1, padding=1), nn.ReLU(),
# 384 * 12 * 12 -> 384 * 12 * 12
nn.Conv2d(in_channels=384, out_channels=384, kernel_size=3, stride=1, padding=1), nn.ReLU(),
# 384 * 12 * 12 -> 256 * 12 * 12
nn.Conv2d(in_channels=384, out_channels=256, kernel_size=3, stride=1, padding=1), nn.ReLU(),
# floor((12 - 3 + 2) / 2) = 5
# 256 * 5 * 5
nn.MaxPool2d(kernel_size=3, stride=2)
)
# fully connect part
self.fc = nn.Sequential(
nn.Linear(256 * 5 * 5, 4096),
nn.ReLU(),
# Use the dropout layer to mitigate overfitting
nn.Dropout(p=0.5),
nn.Linear(4096, 4096),
nn.ReLU(),
nn.Dropout(p=0.5),
# Output layer.
# the number of classes in Fashion-MNIST is 10
nn.Linear(4096, 10),
)
def forward(self, img):
feature = self.conv(img)
output = self.fc(feature.view(img.shape[0], -1))
return output
模型訓練雖然論文中AlexNet使用ImageNet數據集,但因為ImageNet數據集訓練時間非常長,我們使用Fashion-MNIST數據集來演示AlexNet。讀取數據的時候我們額外做了一步將圖像高和寬擴大到AlexNet使用的圖像高和寬224。這個可以通過torchvision.transforms.Resize實例來實現。也就是說,我們在ToTensor實例前使用Resize實例,然後使用Compose實例來將這兩個變換串聯以方便調用。
def load_data_fashion_mnist(batch_size, resize=None, root='~/Datasets/FashionMNIST'):
"""Use torchvision.datasets module to download the fashion mnist dataset and then load into memory."""
trans = []
if resize:
trans.append(torchvision.transforms.Resize(size=resize))
trans.append(torchvision.transforms.ToTensor())
transform = torchvision.transforms.Compose(trans)
mnist_train = torchvision.datasets.FashionMNIST(root=root, train=True, download=True, transform=transform)
mnist_test = torchvision.datasets.FashionMNIST(root=root, train=False, download=True, transform=transform)
if sys.platform.startswith('win'):
num_workers = 0
else:
num_workers = 4
train_iter = torch.utils.data.DataLoader(mnist_train, batch_size=batch_size, shuffle=True, num_workers=num_workers)
test_iter = torch.utils.data.DataLoader(mnist_test, batch_size=batch_size, shuffle=False, num_workers=num_workers)
return train_iter, test_iterload_data_fashion_mnist()方法定義了讀取數據的方式,Fashion-MNIST原來是1 × 28 × 28的大小。resize在原圖的基礎上修改了圖像的大小,可以將圖片調整為我們想要的大小。
def train(net, train_iter, test_iter, batch_size, optimizer, num_epochs, device=mlutils.try_gpu()):
net = net.to(device)
print("training on", device)
loss = torch.nn.CrossEntropyLoss()
timer = mlutils.Timer()
# in one epoch, it will iterate all training samples
for epoch in range(num_epochs):
# Accumulator has 3 parameters: (loss, train_acc, number_of_images_processed)
metric = mlutils.Accumulator(3)
# all training samples will be splited into batch_size
for X, y in train_iter:
timer.start()
# set the network in training mode
net.train()
# move data to device (gpu)
X = X.to(device)
y = y.to(device)
y_hat = net(X)
l = loss(y_hat, y)
optimizer.zero_grad()
l.backward()
optimizer.step()
with torch.no_grad():
# all the following metrics will be accumulated into variable `metric`
metric.add(l * X.shape[0], mlutils.accuracy(y_hat, y), X.shape[0])
timer.stop()
# metric[0] = l * X.shape[0], metric[2] = X.shape[0]
train_l = metric[0]/metric[2]
# metric[1] = number of correct predictions, metric[2] = X.shape[0]
train_acc = metric[1]/metric[2]
test_acc = mlutils.evaluate_accuracy_gpu(net, test_iter)
if epoch % 1 == 0:
print(f'epoch {epoch + 1} : loss {train_l:.3f}, train acc {train_acc:.3f}, test acc {test_acc:.3f}')
# after training, calculate images/sec
# variable `metric` is defined in for loop, but in Python it can be referenced after for loop
print(f'total training time {timer.sum():.2f}, {metric[2] * num_epochs / timer.sum():.1f} images/sec ' f'on {str(device)}')在整個程序的main()方法中,先定義網絡,再使用load_data_fashion_mnist()加載訓練和測試數據,最後使用train()方法進行模型訓練:
def main(args):
net = AlexNet()
optimizer = torch.optim.Adam(net.parameters(), lr=args.lr)
# load data
train_iter, test_iter = mlutils.load_data_fashion_mnist(batch_size=args.batch_size, resize=224)
# train
train(net, train_iter, test_iter, args.batch_size, optimizer, args.num_epochs)
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Image classification')
parser.add_argument('--batch_size', type=int, default=128, help='batch size')
parser.add_argument('--num_epochs', type=int, default=10, help='number of train epochs')
parser.add_argument('--lr', type=float, default=0.001, help='learning rate')
args = parser.parse_args()
main(args)其中,args為參數,可以在命令行中傳遞進來。
我將原始碼上傳到了GitHub(https://github.com/luweizheng/machine-learning-notes/tree/master/neural-network/cnn)上,並提供了PyTorch和TensorFlow兩個版本。
參考資料
Krizhevsky, A., Sutskever, I., & Hinton, G. E. (2012). Imagenet classification with deep convolutional neural networks. In Advances in neural information processing systems (pp. 1097-1105).http://d2l.ai/chapter_convolutional-modern/alexnet.htmlhttps://tangshusen.me/Dive-into-DL-PyTorch/#/chapter05_CNN/5.6_alexnet