[ML]CS231n面向视觉识别的卷积神经网络之学习笔记1

1. 背景

CS231n的全称是:
Convolutional Neural Networks for Visual Recognition
面向视觉识别的卷积神经网络

这篇文章只是记录了自己学习 cs231n 课程的一些笔记或者自己的一些想法。如果想看完整的笔记,知乎有人做了很详细的翻译版本,请移步这里

机器学习翻来覆去,其实就是那么些算法,这门课,主要是讲在视觉识别领域,如何使用这些算法进行识别和调优。从最简单的KNN到神经网络以及卷积神经等。说白了,如果这些算法已经掌握的很牛逼了,那么我感觉,高手们学习这门课大概也就几个小时的时间,就可以吃透了。

2. 视频链接

斯坦福官方链接
网易云课堂翻译版本

内容其实是一样的,只是英文不是很好的同学,可以看这个网易云课堂的版本

3. 正文:深度学习与计算机视觉

3.1 引入

3.1.1 图像分类算法总览

拿到一个图像,我们怎么知道他是个动物还是风景,是张三还是隔壁老王。因为我们的数据库不可能有一模一样的图像,所以只能看它和哪个或者哪些图像最接近,我们就把他归类到这种图像中。
这种行为称之为“分类”。那我们怎么分类呢?分类有哪些算法呢?

机器学习中的分类算法非常多:


这个图基本列举了常见的machine learning的算法,大体分为4类,分别是:
classification (分类)
clustering (聚类),
regression (回归),
dimensionality reduction (降维)

其中,
分类和回归的区别是:分类是离散的,回归是连续的。
分类和聚类的区别是:分类是监督学习(有可供参考的类别标签的),聚类是无监督学习(没有可供参考的类别标签);

补充1:回归也属于监督学习,聚类和分类都是离散的。
补充2:降维,这块还没学懂,有监督的算法也有无监督的算法。暂且不展开讨论。

这里不会给大家展开所有的算法,只是先整体有个大概,课程真正的展开是从KNN开始的,
这个是个很简单的算法,从这里入门,便于后续的深入研究。

3.1.2 学习前的准备

首先想这么一个问题:

我们如何比较2幅图的区别?

比较直观的思路是:图的本质就是RGB的组合,每个像素点就是一个RGB值,然后以像素点为单位,一个图就可以扩展为一个m*n的矩阵(假设横向m个像素,纵向n个像素),为了方便比较,我们统一把所有的图都可以整理为m*n的矩阵。那么图像分析问题就转换为矩阵比较问题:

如何确认2个矩阵是相近或者属于同类的?

我的第一反应就是,直接求差。你想啊,如果2个矩阵完全相等,那么求差的结果就是个零矩阵啊。就像手写数字识别一样,你的test数据转换为矩阵A,如果train数据为矩阵B, A-B=[0,…0],那么A样本必然和B属于同类啊。

Notes: 这个思路扩展下去,其实就是2个矩阵求曼哈顿距离(后面会谈到的L1公式)


屏幕快照 2017-04-25 上午12.01.21.png

但这个是理想情况,那么如果test数据和train数据比较发现,和好几个样本的差值都比较接近,(而且关键的一点是,差值可正可负),那怎么定义A的分类呢?
如此说来,求差值必然是不完整的,那么怎么办?差值求出来求绝对值?嗯,好像有点接近,但本质上和直接求差没有太大区别。
那么换个思路,我们放到几何中去考虑,其实本质就是求2点距离的问题,对吧。 这个从小就学过,点A和点B的距离,横纵坐标差的平方和再开方。

Notes: 这个思路扩展下去,其实就是2个矩阵求欧式距离(下面会谈到的L2公式)

那么总结下来,好像我们也不需要懂什么机器学习的理论,客观分析,其实也能大概想到一些解决类似问题的方法。
这个思路扩展下去,就是CS231n后面要讲的KNN算法。当然实际算法,在距离的算法上有一些新的扩展,但原理都是一样的。

3.2 机器学习的常见算法实现


3.2.1 KNN临近算法(K-Nearest Neighbor)

3.2.1.1 理论

对应知乎笔记:CS231n课程笔记翻译:图像分类笔记(上)

这个算法的本质就是“近朱者赤”,从临近范围内(这个范围可以通过L1公式或者L2公式进行计算)的对象看看哪个占比高,就用那个作为其类别。
其中的关键是K,就是选择几个样本的问题,本质就是临近范围选择多少的问题。

L1,L2公式说明:
L1:曼哈顿距离(wiki解释)
简单说就是矩阵直接求差,该公式俗称出租车距离,地图上A.B两点的距离,出租车不可能直线穿过去,只能横向纵向走,所以就有这个公式。
L2:欧式距离(wiki解释
这个应该大家都学过(空间2点的直线距离),这里不赘述了。

课程的后半段:
其中提到了几个建议的方法,比如交叉验证什么的,这些都是一些优化训练的方法,这会知道了最好,不知道也没关系,实际把数据玩起来,这些方法无形中自然会知道。

所以本章节的关键是,知道KNN算法,并且能实现它。然后知道他在实际中并不怎么用。(毕竟这算法也太简单了,做出来的结果准确率太低啊)。 导师还专门强调了不要在图像识别上使用KNN算法。

3.2.1.2 KNN算法 python实现

知道原理, 算法实现起来很简单。 这里简单交代下背景:

inX: 是输入的测试向量
dataSet: 是训练数据集合(array)
labels: 是训练样本的对应标签(array)
k: 是选择样本的个数
其他:python的tile函数用于生成指定维度的重复数组,sum(axis=1)是按行求和

然后,实现算法:
1.求距离 2. 按距离排序 3.按照k值取前k个使用

#!/usr/bin/python
# -*- encoding: utf-8 -*-

from numpy import *
import operator
from os import listdir

def classify(inX, dataSet, labels, k):
dataSetSize = dataSet.shape[0]

# 1、calculate distance
diffMat = tile(inX, (dataSetSize, 1)) - dataSet
sqDiffMat = diffMat ** 2
sqDistances = sqDiffMat.sum(axis=1)
distances = sqDistances ** 0.5

# 2、sort from min-> max
sortedDistIndicies = distances.argsort()

# 3、confrim the frequency
classCount = {}
for i in range(k):
  voteIlabel = labels[sortedDistIndicies[i]]
  classCount[voteIlabel] = classCount.get(voteIlabel, 0) + 1

# 4、get result
sortedClassCount = sorted(classCount.iteritems(),
key=operator.itemgetter(1), reverse=True)

关键步骤:

最为关键的是计算目标矩阵和临近矩阵的距离,上面的代码只是一个普通的实现方式,如果深思的话,会有多种实现方式.
这门课程的作业1,要求通过3种循环方式实现距离的计算:

def predict(self, X, k=1, num_loops=0):
    # 根据传入的num_loops,决定使用哪种计算方式
    if num_loops == 0:
        dists = self.compute_distances_no_loops(X)
    elif num_loops == 1:
        dists = self.compute_distances_one_loop(X)
    elif num_loops == 2:
        dists = self.compute_distances_two_loops(X)
    else:
        raise ValueError('Invalid value %d for num_loops' % num_loops)

    return self.predict_labels(dists, k=k)

这个东西,上来就让人一脸懵逼(对numpy熟悉的同学除外),本身绞尽脑汁才想出来一种最为普通的方式。
现在却还要用到不同的循环方式,完全是get不到点在哪里。

后来查了很多的资料,各种stackoverflow,各种blog才是搞清楚。主要原因是本人对numpy完全不熟悉,所以对于矩阵的计算不甚了解,之前一直仅仅是把矩阵当做数组来说思考处理的,但实际numpy提供了很多便捷的方法。

言归正传: 本质是求两个矩阵的欧氏距离,那我们的矩阵是长什么样的呢?
我们的数据来源是kaggle的CIFAR-10数据集,通过下面这个shell脚本,(或直接去kaggle搜索CIFAR-10),获取数据:

# Get CIFAR10
wget http://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz
tar -xzvf cifar-10-python.tar.gz
rm cifar-10-python.tar.gz 

下载后可以看到我们有将近5、6k个图片,每个图片是32*32像素的,然后,上文提到过,我们每个图片本质就是RGB,所以还需要每个像素3个RGB值,这样表示我们的图片集,可以用一个5000*32*32*3的四维矩阵来表示,直观的看就是这样:

屏幕快照 2017-04-25 上午12.47.51.png

对应的矩阵,直观感受下就好:

Training data shape:  (50000, 32, 32, 3)
[[[[  59.   62.   63.]
   [  43.   46.   45.]
   [  50.   48.   43.]
   ..., 
   [ 158.  132.  108.]
   [ 152.  125.  102.]
   [ 148.  124.  103.]]

  ..., 
  [[ 198.  190.  170.]
   [ 189.  181.  159.]
   [ 180.  172.  147.]
   ..., 
   [ 178.  171.  160.]
   [ 175.  169.  156.]
   [ 175.  169.  154.]]

  ..., 
  [[ 150.  143.  135.]
   [ 140.  135.  127.]
   [ 132.  127.  120.]
   ..., 
   [ 224.  222.  218.]
   [ 230.  228.  225.]
   [ 241.  241.  238.]]

  [[ 122.  119.  114.]
   [ 118.  116.  110.]
   [ 120.  116.  111.]
   ..., 
   [ 179.  177.  173.]
   [ 164.  164.  162.]
   [ 163.  163.  161.]]]]
算法1:compute_distances_two_loops:

看到这个矩阵和图片之后,估计大家可以较为简单的想到一个计算方法:就是每个图片彼此求距离啊,那么假设有10个测试数据,20个训练数据,那么2个for循环,取出来图片一一求距离,就可以了:

# 代码比较简单,就不细说了
def compute_distances_two_loops(self, X):
    num_test = X.shape[0]
    num_train = self.X_train.shape[0]
    dists = np.zeros((num_test, num_train))
    for i in xrange(num_test):
        for j in xrange(num_train):
            dists[i, j] = np.sqrt(
                np.sum(np.square(self.X_train[j, :] - X[i, :])))

    return dists
算法2:compute_distances_one_loop:

其实,第一个算法想出来之后,第二个原理上是和第一个一样的, 也是取出来测试的图片,逐个和训练集求距离,只是基于numpy的方法,可以更简单的实现:

diffValue = X[i, :] - self.X_train

所以用一个循环实现的距离计算代码是:

def compute_distances_one_loop(self, X):
    num_test = X.shape[0]
    num_train = self.X_train.shape[0]
    dists = np.zeros((num_test, num_train))
    for i in xrange(num_test):
        diffValue = X[i, :] - self.X_train
        dists[i, :] = np.sqrt(np.sum(diffValue**2))

    return dists
算法3:compute_distances_no_loops:

最后一个不用循环,直接求距离的方法,其实是对考察对数学的了解程程度了,两个矩阵的欧式距离:

||v-w||^2 = ||v||^2 + ||w||^2 - 2*dot(v,w)

看着很像平方差公式,对不对,知道这个,翻译成python代码就很简单了:

def compute_distances_no_loops(self, X):
        num_test = X.shape[0]
        num_train = self.X_train.shape[0]
        dists = np.zeros((num_test, num_train))

        # ||v-w||^2 = ||v||^2 + ||w||^2 - 2*dot(v,w)

        """ 
        Soluton1:
        """
        dists = np.sum(X * X, axis=1)
        test_square = np.reshape(
            np.sum(X * X, axis=1), (-1, 1))  # (num_test,1)
        train_square = np.reshape(
            np.sum(self.X_train * self.X_train, axis=1), (1, -1))  # (1,num_train)
        dists = test_square + train_square - 2 * \
            np.dot(X, np.transpose(self.X_train))  # (num_test,num_train)
        dists = np.sqrt(dists)

        """
        Soluton2:
        """
        M = np.dot(X, self.X_train.T)
        te = np.square(X).sum(axis=1)
        tr = np.square(self.X_train).sum(axis=1)
        dists = np.sqrt(-2 * M + tr + np.matrix(te).T)
        dists = np.array(dists)

        return dists

源码地址: https://github.com/polegithub/standford_cs231n_assignment

以上是KNN的算法,并通过3种循环方式分别进行了实现,当然这3种算法,效率上也是有很大差别的,我实际跑下来的时间:

屏幕快照 2017-04-25 上午1.28.11.png

可以看到,向量的实现算法比其他算法在效率上有显著的优势。
至于为什么? 后面抽个专题讲讲运行效率吧。

3.2.1.3 总结

总体来说,KNN应该是入门里面最为容易理解的算法之一了,不太需要太多的数学功底,基本就是你如何进行矩阵求差的问题。实际工作中虽然也用的不多,但是如果你数据量不大,而且只是想先大概估算某个数据的大致概率或者分布,可以临时用这个快速实现一些简单需求。

3.2.1 线性函数:SVM算法(支持向量机)

3.2.1.1 理论

i> Support vector machine, 支持向量机
所属类别: supervised learning 监督学习
所属范畴: classification

ii> 先放链接:
wiki解释
知乎解释

iii> 具体要解释的话,套用窦娥的一句话:

天,你不分黑白何为天; 地,你不辨忠奸枉为地

简单说,就是个分类算法,画地为牢,寻找一条分割线,分出类别。那怎么找出来这条线呢?
桌子上有一堆苹果和香蕉,给你一把尺子,最大限度的将其分开,那怎么分? 这个很简单,直接从最中间的点入手,左右调整调整就找出最可靠的分法了对吧,基本是这样一张图:

屏幕快照 2017-05-02 上午12.10.33.png

那如果从数学或者几何的角度怎么理解这个东西呢?
首先,苹果和香蕉离得最近或者有重合的部分,沿着这些点,基本能大致确定分割线的方向和路径,然后做一些微调就好。那这样的一件事或者一个行为,称之为machine,其中,离这个分割线比较的苹果或者香蕉称之为supooer vector(支持向量),因为这些support vector的友情支持和协助,才能方便我们快速确定分割线。(说白了,简单粗暴点,直接把support vector连点成线,其实就已经是个分割线了,只是有点糙而已)
所以SVM就是“支持向量”的“机器(分类器)”,千万不要理解成“支持”了“向量机”

iv> 数学计算
知道了定义,那么数学角度,怎么求出这么一条分割下来?
首先来分析,就这条线,处于最中间的位置,意思就是距离是距离两边是相等的,或近似相等的。转化为求distanceApple和distanceBanana相等的曲线函数:
转来一段知乎大拿的解释

屏幕快照 2017-05-02 上午12.24.28.png

其实关键的部分是SMO相关的数学推导,推荐大家可以看一下这个博客:
支持向量机通俗导论(理解SVM的三层境界)

最终的损失函数公式是:

屏幕快照 2017-05-05 上午12.13.32.png

v> python 实现
一言不合上代码:
把上面的公式一步步翻译成代码:

  • 交代背景:
# 我们有N个图片样本,C种类别,D个维度
# classes = ['plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck']
# D = 32 * 32 *3  (每个图32*32像素,每个像素有RGB3种颜色构成) 

  - W: A numpy array of shape (D, C) containing weights.
  - X: A numpy array of shape (N, D) containing a minibatch of data.
  - y: A numpy array of shape (N,) containing training labels; y[i] = c means
    that X[i] has label c, where 0 <= c < C.
  - reg: (float) regularization strength
  • 要实现的公式:


    屏幕快照 2017-05-05 上午12.13.32.png
  • 核心公式f(xi;W) 实现:
     scores = X[i].dot(W)
  • 那么max(0,f(xi;W)j - f(xi;w)yi + △)的实现就是:
      margin = scores[j] - correct_class_score + 1 # note delta = 1
      return margin > 0 ? margin : 0
  • 所以SVM的简易版本的实现就是:
def svm_loss_naive(W, X, y, reg):
  dW = np.zeros(W.shape) # initialize the gradient as zero

  # compute the loss and the gradient
  num_classes = W.shape[1]
  num_train = X.shape[0]
  loss = 0.0
  for i in xrange(num_train):
    scores = X[i].dot(W)
    correct_class_score = scores[y[i]]
    for j in xrange(num_classes):
      if j == y[i]:
        continue
      margin = scores[j] - correct_class_score + 1 # note delta = 1
      if margin > 0:
        loss += margin

  loss /= num_train

  # Add regularization to the loss.
  loss += reg * np.sum(W * W)

  return loss, dW
  • 上面的简易版本,使用了一次循环,如果从效率的角度,可以通过向量实现,来避免循环:
def svm_loss_vectorized(W, X, y, reg):
  loss = 0.0
  dW = np.zeros(W.shape) # initialize the gradient as zero

  scores = X.dot(W)        # N by C  样本数*类别数
  num_train = X.shape[0]
  num_classes = W.shape[1]

  scores_correct = scores[np.arange(num_train), y]
  scores_correct = np.reshape(scores_correct, (num_train, 1))  # N*1 每个样本的正确类别

  margins = scores - scores_correct + 1.0     # N by C   计算scores矩阵中每一处的损失
  margins[np.arange(num_train), y] = 0.0      # 每个样本的正确类别损失置0
  margins[margins <= 0] = 0.0                 # max(0, x)
  loss += np.sum(margins) / num_train         # 累加所有损失,取平均
  loss += 0.5 * reg * np.sum(W * W)           # 正则

  # compute the gradient
  margins[margins > 0] = 1.0                  # max(0, x)  大于0的梯度计为1
  row_sum = np.sum(margins, axis=1)           # N*1  每个样本累加
  margins[np.arange(num_train), y] = -row_sum  # 类正确的位置 = -梯度累加
  dW += np.dot(X.T, margins)/num_train + reg * W     # D by C

  return loss, dW

源码地址: https://github.com/polegithub/standford_cs231n_assignment

vi> SVM 总览
SVM展开来说可以参照下图:


屏幕快照 2017-05-01 下午11.20.41.png

待续...

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345

推荐阅读更多精彩内容