CS231,TensorFlow

CS231n代码:https://github.com/GreenArrow2017/MachineLearning/tree/master/CS231n
TensorFlow代码:https://github.com/GreenArrow2017/MachineLearning/tree/master/tf

Image Classification pipline

课程第一章啥也没讲,第二章开始。以图片分类为主题,逐步引出KNN,线性分类等算法。图片数据使用CIFAR-10的数据,计算机扫描图片只能看到一个个像素点,如果是彩色图片,那就是一个三维图片矩阵,如果是黑白图片那就是二维。

CIFAR-10要求给出某张图片的分类,要求很简单,但是并不意外着做起来简单。首先是图片多角度的问题,一只猫也有不同角度的照片,这些照片能否全部识别成猫对于计算机来说是一个挑战。还有照片尺寸大小,或者是图片角色不同姿势等等。CIFAR-10数据集有十个种类,每一个种类提供50000训练数据和10000的测试数据。

KNN

这个算法老早前就实现过:https://www.jianshu.com/p/80192eda92a8
这里只不过是简单提及一下而已,对于相似度算法,我觉得没有哪个老师讲的比林轩田老师的数据《learning from data》要好了,特别是KNN的理论推导方面还有模型演化写的很好。KNN算法属于半参数模型,这类模型没有训练过程,预测过程时间比一般的参数模型的时间要更多,相对于把训练模型的时间转移到了预测阶段。
事实上这种算法在图片识别上很少使用,预测效果并不好。KNN算法的关键就在于如何比较双方图片的差距了,比较差距的方法很多,在推荐系统中也学了不少,就是不知道能不能用上。KNN最主要的比较手段就是L1和L2距离:


这两种距离有点像正则化的w1和w2,所以我一开始觉得区别应该就是能不能求导的问题,d1边界不平滑,难求导;而d2边界就是一个圆,可以求导。


课程还提到另一种区别,之前没有想到的。d1距离类似一个菱形,这里展示的是l1和l2的二维图,当坐标轴发生变化,转一个角度,那么d1距离就会发生变化,也就是说d1距离和坐标轴以及当前度量单位有关。而d2完全没有这种问题,怎么转都一样。有这样的区别自然适用情况也有差别,如果数据带有意义,比如预测升高,一个人的腿长,身长都是有具体意义的,那么最好使用d1,如果数据没有特别的意义,比如因子分解机分解出来的属性(推荐系统),这些属性数据可以人为给定意义,比如电影的剧情,喜剧程度等等,也可以不给,因为本身就没有意义,为了方便解释给定意义而已,那么久可以用d2。当然了,大多数情况最好还是两个都试一下。
代码测试,首先下载CIFAR-10的数据,然后处理一下:

def unpickle(file):
    import pickle
    with open(file, 'rb') as fo:
        dict = pickle.load(fo, encoding='bytes')
    return dict

返回的是一个字典,由于读出的二进制文件,在读取字典需要加d。


以k=1,d1距离为例实现算法,其他的只要改改就好了。

由这个最简单的NearestNeighbor可以演变成KNN,让其附近的K个数据点投票即可。对于k的选择其实是越多越好,但问题就在于k越多,计算量会指数增长,比如k=3到k=5,效益指数增加了百分之0.03(具体推导在learning from data中),计算量确打了不止一个级别,性价比太低。所以正常情况下还是选择k=3即可。
KNN算法根本不适合用于图像识别,首先是计算量的问题,KNN算法的好处在于其最坏分类的结果不会超过最后分类结果的2倍,具体推导在https://www.jianshu.com/p/80192eda92a8
但是这是建立在数据点密集的情况下,数据是二维还好,数据要是三四五维,那得多少数据才能填满这些空间。其次就是图片相似度的问题,图片相似度未必能用d1,d2距离测量的准确,比如一张图片是汽车和大片天空,另一张图片也是马和大片天空,这两张图片角色不同,但是天空占大多数,所这两张图片会归为一类,自然是不对的。

Validation sets for Hyperparameter tuning

课程提到的最后一个问题是选择超参数的问题,KNN算法的k参数需要在训练前指定,问题来了,然后测定K值?课程上老师专门提了一句:we cannot use the test set for the purpose of tweaking hyperparameters,不能动用测试数据来测定超参数。在选择算法训练数据的时候,务必要把测试数据当成是一种很珍贵的资源,不到最后不能使用。
有一种看似合理的做法,把数据集分成两份,一份训练数据,一份测试数据,用每一个k和训练数据来训练模型,然后放到测试数据里面选择最好的。这样看起来好像是合理的,但是test集合未必就能代表所有的样本了,所有有可能只选择了适合在当前样本的数据而已,这样也是不可取的。


另一钟更常见的做法是KFold交叉验证,这种做法在深度学习不常用,毕竟计算量太大了,但是对于机器学习来说不失为一种很好的做法。

代码GitHub:https://github.com/GreenArrow2017/MachineLearning/tree/master/CS231n

Loss Functions and Optimization

课程前面还有一点提到了线性分类器,就是函数,课程特意介绍了一下对于b的作用,b可以用来调整类别数量上的不足。对于的每一行上的每一个元素都会告诉我们对应图片的像素对分类有多少影响。
线性模型可以解释成是每一个种类学习的模板,图片的每一个像素都对应一个权重,告诉我们这个像素的影响有多大。还有一种高纬度的解释,线性分类器代表了高纬度空间的一个分类边界。
接下来的关键就是这么找到w。可以用一个函数作为w的输入,定量估计一下w的好坏,这个函数称为loss function。二元SVM的损失函数已经学习过,现在有十个类别,拓展到十个类别多元SVM。SVM使用hinge loss function,这种损失函数存在一个安全边界。这里multiSVM也是一样
给出的一就类似SVM二元里面的ξ,这里的ξ和SVM二元不一样,当时,当前点就睡支持向量,这个时候ξ是0,如果,那么ξ=0,当前点分类正确,并且这个点远离分类边界。当,这个就不能确定了。但是在这里这些没有特别的意义,只是类比了SVM的hinge loss function而已。



如上例子,第二幅图片稍微修改一下4.9,score还是没有什么变化。课程还提了另外一个问题,loss function的值域是多少,这个就非常明显了,直接把hinge loss的图像画出来,。另外一个问题是,当模型初始化训练的时候,经常会出现Score分数之间都差不多的情况。这个时候再使用multiSVM的时候得到的loss是多少?当趋向于所有的score都是趋向0的时候,那么si-sj+1=1,所以,加起来就会得到类别的数量-1。这个结论可以在debug的时候测试一下代码是否正确。中间还有几个问题合页函数的平方有时候也会作为一个技巧,比如0.1的损失是可以不处理的,平方之后惩罚力度就更小了,如果是10平方之后惩罚力度更大了。个人感觉可能更好。

这个问题比较有意思,如果算出来的score是唯一的,w是不是唯一的?这个肯定不一定是唯一的,因为w放大缩小相同倍数也是可以得到的。

overfitting


接下来就是过拟合了,这些都是基础知识。如果模型只是一味的进行拟合,模型可能会很曲折。事实上我们关心的不是训练数据的表现,而是测试数据的表现。这个时候会用regularization来解决这个问题。

也就是奥卡姆剃刀原则。推导也很简单,模型简单其实就是w简单,w简单就是简单,也就是让即可,接下来就回到了拉格朗日乘子法了,target function在加上一个,C是常数去掉即可。这里的可以是其他的函数。至于为什么要限制w,因为在vc 维的推导中w的维度+1就是vc维的限制边界了,限制了w也即是限制了vc维也就限制了复杂度。

regularization

regularization有很多类型

L2范式比较常用,L1范式可以使得w矩阵趋向于稀疏矩阵,和拉普拉斯有点关系,l1和l2意义有些相反。

softmax

除了multiSVM还有另外一种常见的多分类,softmax loss。


所以
接着思考几个问题,的上限和下限是什么,这个问题很好回答,既然是概率,最大最小就是0和1,自然就是infinity和0了。如果在正确类别上对softmax的正确分类上进行些许改变,softmax会有变化,而SVM不会有变化,因为SVM只要正确类别比错误类别高出一个delta即可,而softmax不会,无论高出多少,只要不是1就会一直叠加score朝一的方向进行。

picture feature

还有一个很重要的就是图片特征。前面还有一小节是讲optimization的,就是求导之类的,很基础。之前的特征都是直接把所有的像素都拉长了,直接传进分类器,但是这样做可能不太好。所以在拿到图片时,可以先计算图片的各个特征。在神经网络兴起前,常用的特征向量是方向梯度直方图,因为人们发现,有向边缘很重要


这样的图片可以看出包含了几种颜色,有哪些不同的类型边缘。
还有一种是词袋模型。如果得到了一句话,那么表示这句话的一个方法就是用单词出现在这句话的次数来表示。但是仅仅把文字类比于图片不太合理,所以需要我们自定义我们自己的单词表。首先把图片转换成各个像素,然后用Kmeans等等的聚类方法集合成簇,得到不同的簇中心

聚类完成之后会得到不同的颜色。
最后还有一些优化问题,对于multi-SVM,求导起来就非常简单了

按照这个公式进行梯度下降就好了。

def loss(x, y, reg, w):
    num_train = x.shape[0]
    score = x.dot(w)
    correct_score_class = score[range(num_train), list(y)].reshape(-1,1)
    margins = np.maximum(0, score - correct_score_class + delta)
    margins[range(num_train), list(y)] = 0
    loss = np.sum(margins) / num_train + 0.5 * reg * np.sum(w * w)

    num_classes = w.shape[1]
    gradient = np.zeros((num_train, num_classes))
    gradient[margins > 0] = 1
    gradient[range(num_train), list(y)] = 0
    gradient[range(num_train), list(y)] = -np.sum(gradient, axis=1)
    

    dw = (x.T).dot(gradient)
    dw = dw / num_train + reg * w
    return loss, dw

softmax也一样


求导也要分两种情况,这算是很简单的情况了,SVM后面的smo算法求导真的写到爆肺。

#softmax multi-classification

def getLossAndDW(x, y, weight):
    LOSS = 0.0
    n = x.shape[0]
    score = x.dot(weight)
    correct_class = score[np.arange(n), y].reshape(n, 1)
    exp_sum = np.sum(np.exp(score), axis=1).reshape(n, 1)
    LOSS += np.sum(np.log(exp_sum) - correct_class)
    LOSS /= n
    LOSS += 0.5 * reg * np.sum(w ** 2)

    dweight = np.exp(score) / exp_sum
    dweight[range(n), list(y)] -= 1
    dweight = (x.T).dot(dweight) / n + reg * weight
    return LOSS, dweight

代码就很直接了。超参数什么的懒的选取了,反正训练步骤也就那样,垃圾电脑跑半天跑不出来,CIFAR数据前四份训练最后一份测试。

def train_softmax(weight, losses, acc,time=10):
    num_train = 200
    for k in range(time):
        for i in range(4):
            start = 0
            print("%d training data : " % (i + 1))
            for j in range(50):
                x = trainData[i][start:num_train + start]
                y = trainLabel[i][start:num_train + start]
                start += num_train
                loss, dw = getLossAndDW(x, y, weight)
                weight -= learning_rate * dw
                accr = predict(weight)
                if j % 5 == 0:
                   print("loss = ", loss, " ", accr)
                losses.append(loss)
                acc.append(accr)


训练了一万多次吧,每隔100次打一次数据,如果训练时间更长还会一直降,之前在谷歌云训练可以降到0.7多,这里最低才2.0,准确率最高可以跑到42%,现在就不花这么多时间,最高准确率33%。



assignment1还有神经网络的实现,之前都实现过,不过CS231居然给了模板,写完SVM和softmax才发现。

Introduction to Neural Network

Backpropagation

这章是开始神经网络,讲解反向传播的内容,就是求导一步步来。


如上图类似,input -> w -> output,而中间的w是一个雅乐比矩阵,雅克比矩阵是一个由一阶导数组成的矩阵,而方向传播求出来的梯度恰好就是w矩阵要更新的梯度了。

这节课没啥东西,我不知道他一个反向传播求导是怎么讲的55分钟的。主要讲的就是前向传播计算出种类的分数,反向传播计算梯度,最后使用梯度下降修改权值。

Neural Network

简单来说,神经网络其实就是把简单函数通过层次化堆叠而成。激活函数:这个函数图像长的像s,所以也叫sigmod函数,但这个函数随着x的变大y的变化越来越小,容易导致梯度消失的问题,所以慢慢的被淘汰了。梯度消失的情况到这里解决。也容易出现梯度消失。算是relu的一种改进,后面会提到然后选择合适的激活函数。
到目前为止还没有到卷积神经网络,讲的还是全连接。
assignment2的作业3:实现一个两层的全连接神经网络。cs231的作业我都没有使用给的模板,直接就自己写。不过给的模板代码写的真的好。神经网络的架构如下描述。首先是一层输入层,输入层的数据格式(1000,3x32x32),以1000为一个batch,然后就是hidden隐藏层,128个神经元,接着经过一个relu激活函数,最后输出层,还有一个softmax层。input->hidden->relu->output->softmax。
一个五组数据,四组数据一组测试,每一组分成10个batch,一个batch1000个数据,每个batch训练1000次。


初始化代码,超参数设置直接参照之前的项目照抄过来了,毕竟没有上面项目经验,也不知道设置多少。前向传播很简单,softmax的求导实现前面多分类也已经实现过了。

relu函数的参数就是一个x,所以error是对x求导就是1了,1再乘上后面传来的dout即可。反向传播也一样,对x求偏导就是w,对w求偏导为x,相乘即可。

    def training(self, x, y, time=1000):
        for i in range(time):
            l0 = x
            l1 = self.front_propagation(l0, self.w0)
            l1_relu = self.relu(l1)
            l2 = self.front_propagation(l1_relu, self.w1)
            loss, dout = self.softmax(l2, y, self.w0, self.w1)

            if (i == time - 1):
                for k in range(int(time / 100)):
                    print("=", end="=")
                    pass
                print("| 100.0%")

            if (i % 100 == 0):
                print("loss : {:.6f}".format(loss))
                for k in range(int(i / 100)):
                    print("=", end="=")
                    pass
                print(" ", (i / time) * 100, "%")

            dout, dw1 = self.backpropagation(l1_relu, self.w1, dout)

            dout = self.relu_backpropagation(dout, l1)

            dout, dw0 = self.backpropagation(l0, self.w0, dout)

            dw0 += self.reg * self.w0
            dw1 += self.reg * self.w1

            self.w0 -= self.learning_rate * dw0
            self.w1 -= self.learning_rate * dw1

            pass
        pass

实现起来还是相对简单的。之前不知道cs231提供了模板,只需要像leecode那样填写主要代码即可,所以就是自己写了。cs231提供的模板代码写的很好,想加层直接加多函数即可,我写的还需保存某些变量,没得比。

Convolutional Neural Network

课程上提到一篇06年论文,可以更加高效的训练神经网络,在初始化神经网络的时候需要很谨慎,才能进行反向传播。文中提到的方法是使用波尔茨曼机来初始化,我也不知道这是什么玩意,但是在机器学习技法课程里面知道有这么种初始化方法,好像叫编码器。
卷积神经网络主要就是卷积层和池化层了。
数据图片结构不变,准备一个卷积核,类似于TCP的滑动窗口,对重合的部分做点积



首先看第一张图片,展示了卷积层的特征图片,每一个部分都是一个神经元,也就是一个filter点积求和之后的值。网格里面的每一个元素都展示了输入是什么样子,也就是把输入的特征最大化展示了,所以从某种意义上说,网格图片展示的是神经元到底在寻找什么。
接下来就是如何滑动了

如一张7x7的数据,filter3x3,如果stride=1,是可以完全覆盖,如果stride=2,也是可以覆盖的,但如果是stride=3,则不能完全覆盖,当然如果硬是要用stride=3也不是不可以,这得使用0填充法了。这里可以使用公式计算,其实我更在乎的是这个图片拉长之后,要怎么个移动法?
通常情况下,零填充还是使用的比较多的,如果不使用0填充,那么数据的结构会收缩的特别快,可能原始数据32x32x3,卷积层之后可能就变成10x10x1的了,这并不是我们想要的,因为卷积核使用了很少的数据表示整个数据,可能会漏掉一些信息,如果用零填充可以让数据减少的不这么快,保留更多的数据。

问你有多少参数,这个问题比较简单。5x5x3一共75个参数,bias还得加上,十个filter,一个1500个参数,然而这是错的,bias只需要一个相同即可,也就是一个filter一个bias即可,所以应该是750+10 = 760。
卷积核在每一个位置,都会取卷积核与图片局部的点积,找到局部特征。
接下来是池化层,池化层是为了使得特征更小并且更容易处理。最常见的有max pooling

这里不再会有重叠,因为只是做一个降低采用率的处理。另一种池化层是均值池化层,但是用的不多,因为在数据的每一个格子都代表了这个神经元的激发程度,池化层可以看成是在某一个局部区域的激活程度,如果是一些识别的一些任务,池化层是最直观的,不管你是想找图片的光线还是什么信息,都可以从池化层直接看出。这些东西都很简单,剩下的时候就是那班学生在提问题了,最后课程还提到了一种新的神经网络方式,完全丢弃池化层和全连接层,只保留了卷积层,或者只用很小的卷积核配上更加深的神经网络。

activate function

Sigmoid function,这个激活函数存在三个问题,首先是会存在梯度消失的情况


如果x值非常大,很容易出现梯度饱和的区间。第二个问题是sigmoid函数不是以零为对称的,这就意味着每一次更新的数据不是全正的就是全负数的,总是只能走一个方向,如下图,sigmoid之后传回来的dx肯定是全正或者全负,这种更新方式跌宕起伏,效率不高。这也是为什么要用0均值数据的原因,也就是数据归一化。

第三个问题便是计算exp(x)计算复杂度问题了,这个问题在前面的全连接神经网络已经体验到了,稍不注意就会出现infinity的数值问题。
tanh(x),这个激活函数也存在梯度消失的问题,但是这个函数0对称的,比sigmoid函数好一点。

relu,这个函数比较好了,也是之前实现函数所用的,这个函数也有不再以0为中心的问题。

因此在一半的区域中都会出现梯度消失的情况

而如果在weight初始化的时候恰好使得数据不再active relu里面,那这部分数据就不会被更新。当学习率很大的时候,这种情况下如果进行大量的更新,那么很多是relu单元会因为进入了dead relu区域而不再被更新。
Leaky relu,这个函数是基于堆relu函数的一些更新,

还有一种是Maxout "Neuron",取两个函数,找他们的最大值即可。但是神经元会翻倍,计算量也会增大。

data processing

数据预处理,0均值化,归一化是比较常用的。0中心化是因为之前所提到的relu,如果碰上了像relu这些函数,有一部分无法更新就麻烦了,可以用数据0中心化调整一下。

weight initialize

权重初始化,如果我们把权重都初始化为0呢?如果权重都初始化为0,那么得到的分数是0,根据链式发展,dw都是0,权值将不会更新。这是不对的,还有bias偏执项,而偏执项是相同的,所以所有的神经元都会做相同的事情。
接下来把作业做了。找了一下发现卷积层和池化层的梯度求解部分并没有找到,不知道是我漏看了还是咋地,跑去看CS231官方笔记之后直接开始做作业了,还是老样子,不用模板自己做吧。
数据初始化就不干了,代入几个公式即可。神经网络的架构:
输入层->卷积层->relu激活函数->pooling->relu激活函数->隐藏层1->隐藏层2->softmax输出
对于卷积层的优化就很简单了,和全连接层是没有区别的,一样;因为还是wx,然后对x求导对w求导,烦就烦在这个filter对应的范围怎么找。我这里全部用for循环,可想而知效率有多低了。对于池化层的优化基本没有上面计算了,比如四个格子2 3 4 5=>5,那么反向传播回来就是0 0 0 5即可。


卷积前向传播,卷积之后的shape使用公式直接求,课程上有提到的,,直接代入即可,剩下的就是迭代了。

也是迭代求解,这里注意迭代的第二个参数是C,也就是RBG三个颜色的迭代,因为池化层是一片一片来的,而之前的filter是一个filter一个filter来。

池化层的反向传播也是一样。

卷积层的反向传播需要注意,因为卷积层经过了pad添加,自然反向传播也需要pad添加才能符合shape,多个filter,自然是吧所有的filter更新出来的w和b加起来了。这里还遇到一些类型问题,中间有一段np.float32类型转换的代码,那段代码是必须的,因为没有这段代码后一段加法代码出错,hint:float32和uint类型不能相加。
而且这里的更新和前面的pool_backword又不一样了。注意第二个参数是filter个数,一开始就是这里的错误调了一个小时。

  def loss(self, x, y=None):
        W1, b1 = self.params['W1'], self.params['b1']
        W2, b2 = self.params['W2'], self.params['b2']
        W3, b3 = self.params['W3'], self.params['b3']

        filter_size = W1.shape[2]
        conv_param = {'stride': int(1), 'pad': int((filter_size - 1) / 2)}
        pool_param = {'pool_height': 2, 'pool_weight': 2, 'stride': 2}

        output, cache_conv = self.conv_front(x, W1, b1, conv_param)

        output, cache_relu = self.relu(output)

        output, cache_pool = self.pooling(output, pool_param)

        pool_shape = output.shape

        n = output.shape[0]

        output = output.reshape(n, -1)

        output, cache_full = self.full_connection(output, W2, b2)

        output, cache_full_relu = self.relu(output)

        output, cache_full_2 = self.full_connection(output, W3, b3)

        loss, dx = self.softmax(output, y, W2, W3)

        print("loss : ", loss)

        dout, dw, db = self.back_propagation(dx, cache_full_2)

        W3 -= self.learning_rate * dw

        b3 -= self.learning_rate * db

        dout = self.relu_back(dout, cache_full_relu)

        dout, dw, db = self.back_propagation(dout, cache_full)

        W2 -= self.learning_rate * dw

        b2 -= self.learning_rate * db

        dout = dout.reshape(pool_shape)

        dout = self.back_pool(dout, cache_pool)

        dout = self.relu_back(dout, cache_relu)

        dx, dw, db = self.back_conv(dout, cache_conv)

        W1 -= self.learning_rate * dw

        b1 -= self.learning_rate * db

        self.params['W3'] = W3
        self.params['b3'] = b3
        self.params['W2'] = W2
        self.params['b2'] = b2
        self.params['W1'] = W1
        self.params['b1'] = b1

        return loss

loss这里按照网络模型把函数一个个堆叠起来即可。这种for循环的实现效率真的慢到爆炸,我之前还天真的想用Java,类似这种for循环来实现,虽然Java比Python快点,但也是杯水车薪。比如每一次100个batch,跑10次,跑了40分钟。

很明显learning_rate选小了,我原来选的1e-6更慢,现在1e-5也是不太行,可能是1e-3到1e-4。

Transfer Learning

如果当前使用的模型是CNN,又不想训练完之后过拟合,那么就需要大量的数据,但是如果使用迁移学习,就可以不使用超大量的数据。

也即是使用ImageNet训练好的CNN神经网络,再根据业务需求精调。


到目前为止,一个卷积的神经网络就出来了,虽然很简陋,效果也不怎么样,但是至少还算是一个能跑的轮子了。

TensorFlow

数据处理

keras可以看成是TensorFlow的一个接口,TensorFlow本身可以看成是一个后端,所以这里学习只是熟悉一下整个TensorFlow。



tensorflow是2.0.0的版本。接下来导入数据,tf自带了一个fashion-mnist数据集,可以使用keras导入。

fashion_mnist = keras.datasets.fashion_mnist
(x_train_all, y_train_all), (x_test, y_test) = fashion_mnist.load_data()
x_valid, x_train = x_train_all[:5000], x_train_all[5000:]
y_valid, y_train = y_train_all[:5000], y_train_all[5000:]

第一次可能需要下载。



显示图片,传入的img_arr要是一个图片h x w的矩阵,如果是向量需要reshape回去,所以又reshape((28, 28)),其次调用imshow函数,cmap即是图片类型,默认是RGB,但是这里黑白图片,自然就是binary了。然后显示即可。

如果想显示多张图片,那就需要用到子图了。



展示的图片的个数是由子图大小觉得的,如果展示的图片太少了或者是label和数据数量不对,那么就需要报错了。接着先展开一幅画面,也就是figure,网格的意思,稍微大一点,要不等等就没有图片都黏在一起。其次就是一张一张图片展示。找到图片的索引,划分网格,也就是subplot函数,然后把图片塞进去,imshow函数,接着把坐标系关了axis('off')函数,后面展示即可。

keras搭建全连接神经网络

这个搭建起来是真的简单,keras的搭建有两种方式,一种是Sequential顺序搭建,另一种是Keras API搭建,这里还是用顺序搭建吧。

model = keras.models.Sequential()
model.add(keras.layers.Flatten(input_shape=[28, 28]))
model.add(keras.layers.Dense(300, activation='relu'))
model.add(keras.layers.Dense(100, activation='relu'))
model.add(keras.layers.Dense(10, activation='softmax'))
model.compile(loss="sparse_categorical_crossentropy",
              optimizer = "adam",
              metrics = ["accuracy"])

首先创建一个Sequential容器,接着添加输入层,这里用Flatten是拉直的意思,因为我们传入的是原图片。如果传入一个向量,那就可以用keras.layers.Input即可。接着就是添加层了,Dense是全连接层,输入不用添加,自动计算,只需要填写输出个数即可。添加完后编译一下即可。这里的优化器选择adam,如果用sgd会很慢。

history = model.fit(x_train, y_train, epochs=10, validation_data=(x_valid, y_valid))

训练即可。

返回的history其实是一堆字典。

数据归一化

from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
x_train_scaled = scaler.fit_transform(x_train.astype(np.float32).reshape(-1, 1)).reshape(-1, 28, 28)
x_valid_scaled = scaler.transform(x_valid.astype(np.float32).reshape(-1, 1)).reshape(-1, 28, 28)
x_test_scaled = scaler.transform(x_test.astype(np.float32).reshape(-1, 1)).reshape(-1, 28, 28)

这个直接调包。这里使用的函数有些不同,fit_transform不但进行归一化操作,而且还吧均值和方差保留下来。而transform只拿上一次数据的方差进行操作。而测试数据和验证数据集都是用训练数据的方差和均值进行归一化的。

回调函数

在训练的过程中,可能要做其他的事情,这个时候就涉及到了回调函数了。常见的几个函数有earlyStopping,ModelCheckpoint,TensorBoard。

logdir = './callbacks'
if not os.path.exists(logdir):
    os.mkdir(logdir)
out_put_model = os.path.join(logdir, 'save_model.h5')
callbacks = [
    keras.callbacks.TensorBoard(logdir),
    keras.callbacks.ModelCheckpoint(out_put_model, save_best_only=True),
    keras.callbacks.EarlyStopping(patience=5, min_delta=1e-3)
]
history = model.fit(x_train, y_train, epochs=10, validation_data=(x_valid, y_valid), callbacks=callbacks)

首先准备一个目录,需要存储日志。其次准备存储模型参数的文件,也就是h5结尾的文件。接着调用即可。注意earlyStopping的参数,patience表示如果连续5次都没有变化则停止,min-delta即阈值了。
训练完成后,用TensorBoard --logdir=callbacks命令在控制台启动即可。

回归模型

这个也比较简单,首先还是导入数据,这里的数据还是用sklearn自带的那个加利福尼亚的房子数据。

然后做归一化,常规操作了。
构建网络使用一种更简单的方法:

model = keras.models.Sequential([
    keras.layers.Dense(30, activation='relu', input_shape=x_train.shape[1:]),
    keras.layers.Dense(1),
])
model.compile(loss='mean_squared_error', optimizer='adam')


需要注意的是

pd.DataFrame(history.history).plot(figsize=(8, 5))

如果不是pandas 0.24版本是没有的,0.23 0.25也都没有这个方法。所以要使用必须要确认是0.24.2版本。

批归一化和Dropout

model = keras.models.Sequential()
model.add(keras.layers.Flatten(input_shape=[28, 28]))
for _ in range(20):
    model.add(keras.layers.Dense(300))
    model.add(keras.layers.BatchNormalization())
    model.add(keras.layers.Activation('relu'))
model.add(keras.layers.Dense(10, activation='softmax'))
model.compile(loss = 'sparse_categorical_crossentropy', optimizer='adam')

即可。批归一化也可以使得梯度消失现象有缓解,因为批归一化可以使得参数更加工整,梯度计算更加精准。
dropout是为了防止过拟合。一般是加在最后一层:

model.add(keras.layers.AlphaDropout(rate=0.5))

keras.layers.AlphaDropout和keras.layers.Dropout都是Dropout,但是AlphaDropout更强大,Dropout之后均值和方差不变,这样就不会改变分布了,也就是归一化的性质没有变,可以和批归一化一起使用。

Wide & Deep模型

数据用稀疏特征和密集特征来构建模型。
稀疏特征是一个离散特征,比如性别,工作类型信息等等,都是离散的,只能从n个值里面选。可以用one-hot编码表示。很有效,在工业界很是常用,但是他需要人工设计,很多时候特征是很多的,再叉乘特征就会很大,所以常常需要选择。还可能过拟合,所有特征叉乘,相当于就记住了所有的样本。

密集特征是用向量表示的特征,之前的one-hot就是离散变成向量。Word2vec就是一总密集特征。可以很方便是计算向量之间的相关性,兼容没有出现过的特征。

Google play推荐模型就是典型的wide & deep模型。
简单点说,就是先用一部分,或者全部的特征经过一到两个神经网络后得到的向量和原来没处理过的特征相结合。
input = keras.layers.Input(shape=x_train.shape[1:])
hidden1 = keras.layers.Dense(30, activation='relu')(input)
hidden2 = keras.layers.Dense(30, activation='relu')(hidden1)
concat = keras.layers.concatenate([input, hidden2])
output = keras.layers.Dense(1)(concat)

model = keras.models.Model(inputs=[input], outputs=[output])

model.compile(loss='mean_squared_error', optimizer='adam')

类似于函数那种编程,就是有点难记,用时还得一个个查文档。
这上面的实现都是一样输入的情况,如果输入不一样又要怎么处理?



有些随便,如果是全特征输入,Input里面是shape = [1:],这里的shape=[5]意思是取5个特征。也就是说两个输入选择5和6个特征。接着就是和之前的一样了。
输入既然有两个,那数据自然也要划分成两个了。

x_train_scaled_wide = x_train_scaled[:, :5]
x_train_scaled_deep = x_train_scaled[:, 2:]
x_valid_scaled_wide = x_valid_scaled[:, :5]
x_valid_scaled_deep = x_valid_scaled[:, 2:]
x_test_scaled_wide = x_test_scaled[:, :5]
x_test_scaled_deep = x_test_scaled[:, 2:]
history = model.fit([x_train_scaled_wide,x_train_scaled_deep], 
                    y_train, 
                    validation_data=([x_valid_scaled_wide, x_valid_scaled_deep], y_valid), 
                    epochs=50)

wide特征取前5个,deep特征取后6个。

超参数

搜索策略:网格搜索,随机搜索,遗传算法搜索,启发式搜索。
网格搜索和随机搜索比较简单,网格搜索就是参数划分几个固定的值然后一个个试,可以并行操作。随机搜索就是不划分成固定的,随机试。
遗传算法:首先初始化几组超参数,训练出模型后把特别差的参数丢了,留下好的。然后把留下的参数集合在一起,交叉,就像生孩子一样,然后做一些微小的调整,这样就出现了下一代集合,不断循环。

TensorFlow基础API

常量API:

t = tf.constant([[1,2,3],[4,5,6]])

存数组存变量都可以,
变量API:

v = tf.Variable([[1,2,3], [2,3,4]])
v.numpy()

numpy取出数组或者其他值,如果需要复制则要用assign函数:

v[0,2].assign(5)

近似求导

自动求导这玩意maxnet也有:

def g(x1, x2):
    return x1*5 + (x2**2)

x1 = tf.Variable(2.0)
x2 = tf.Variable(3.0)
with tf.GradientTape() as tape:
    z = g(x1, x2)
dz_dx1 = tape.gradient(z, x1)
dz_dx2 = tape.gradient(z, x2)
print(dz_dx1, dz_dx2)

首先准备函数,用tf.GradientTape() as tape构造一个求导空间,接着利用tape.gradient得到求导数值即可,参数分别是函数输出以及求导的变量。
但是这样其实是不行的,倒数第二句就会出问题,因为用gradient求出导数只能求一次,求完一次之后就会释放空间没了,所以

dz_dx2 = tape.gradient(z, x2)

会出问题。

with tf.GradientTape(persistent=True) as tape:
    z = g(x1, x2)
dz_dx1 = tape.gradient(z, x1)
dz_dx2 = tape.gradient(z, x2)
print(dz_dx1.numpy(), dz_dx2.numpy())

既然是没有保存,那么让他保存就好了,所以只需要添加persistence=True即可,但是你再后面就需要手动删除del了。但是这样有点麻烦,那就不要一个一个求,直接一起。

def g(x1, x2):
    return x1*5 + (x2**2)

x1 = tf.Variable(2.0)
x2 = tf.Variable(3.0)
with tf.GradientTape(persistent=True) as tape:
    z = g(x1, x2)
dz_dx1 = tape.gradient(z, [x1, x2])
print(dz_dx1)

构建CNN

model = keras.models.Sequential()
model.add(keras.layers.Conv2D(filters=32, kernel_size=3, padding='same', activation='relu', input_shape=(28, 28, 1)))
model.add(keras.layers.Conv2D(filters=32, kernel_size=3, padding='same', activation='relu'))
model.add(keras.layers.MaxPool2D(pool_size=2))
model.add(keras.layers.Conv2D(filters=64, kernel_size=3, padding='same', activation='relu'))
model.add(keras.layers.Conv2D(filters=64, kernel_size=3, padding='same', activation='relu'))
model.add(keras.layers.MaxPool2D(pool_size=2))
model.add(keras.layers.Conv2D(filters=128, kernel_size=3, padding='same', activation='relu'))
model.add(keras.layers.Conv2D(filters=128, kernel_size=3, padding='same', activation='relu'))
model.add(keras.layers.MaxPool2D(pool_size=2))
model.add(keras.layers.Flatten())
model.add(keras.layers.Dense(128, activation='relu'))
model.add(keras.layers.Dense(10, activation='softmax'))
model.compile(loss='sparse_categorical_crossentropy', optimizer='adam', metrics=['accuracy'])

有了之前搭建的经验,搭建这玩意很简单,注意layers.Flatten是拉直数据的意思。注意在每一个输入层都需要输入数据的格式,如果不是输入层,就不需要数据格式了,TensorFlow自己去判断就好了,所以Flatten不用加什么数据格式。

深度可分离卷积

在Google的InceptionV3的网络结构:



首先由很好的视野域,其次训练效率高。受Inception的影响,于是便出现了深度可分离卷积



代码要修改很简单。

循环神经网络

Embedding与变长输入处理

与embedding相似的是one-hot编码,one-hot需要一个字典,查看当前词语的位置,当前词语位置则是1,其他位置是0。很明显是稀疏编码。那么对应的就有密集编码了,我们称为embedding,也就是Dense Embedding。
变长输入是padding,在CNN就提过了。在NLP里也是一样,比如一个词的向量是[3,2,4,1],想扩张成8的长度,那就只需要补全0既可。
以Keras的电影情感分析为例子,首先导入数据。

imdb = keras.datasets.imdb
vocab_size = 10000
index_from = 3
(train_data, train_labels), (test_data, test_labels) = imdb.load_data(num_words = vocab_size, index_from = index_from)

输入的num_words是指频率,频率排在10000前的组成词表。从排行第三开始组成词表。由于数据都是句子,所以他们的shape不会一样的,这个时候就需要padding了。

这样看着有费眼睛,把他转换成文字吧。因为是从序号三开始,所以前面是空出了4个,可以用于特殊字符的存储。

word_index[''] = 0
word_index[''] = 1
word_index[''] = 2
word_index[''] = 3
reverse_word_index = dict([value, key] for key, value in word_index.items())
reverse_word_index

生成一个反过来的字典。



然后把数据补全了:

max_length = 500
train_data = keras.preprocessing.sequence.pad_sequences(
     train_data,
     value = word_index[''],
     padding = 'post',
     maxlen = max_length
) 

test_data = keras.preprocessing.sequence.pad_sequences(
     test_data,
     value = word_index[''],
     padding = 'post',
     maxlen = max_length
)

定义模型:

embedding_dim = 16
batch_size = 128
model = keras.models.Sequential()
model.add(keras.layers.Embedding(vocab_size, embedding_dim, input_length=max_length))
model.add(keras.layers.GlobalAveragePooling1D())
model.add(keras.layers.Dense(64, activation='relu'))
model.add(keras.layers.Dense(1, activation='sigmoid'))
model.summary()

Embedding层:首先定义了一个数组,vocabulary_size x embedding大小的矩阵,其实就是one-hot编码的矩阵,然后每一个句子就去这个矩阵查,拼接成一个max_length x embedding的矩阵,所以embadding层得到的是一个batch_size x max_length x embedding的矩阵。
GlobalAveragePooling1D层:Embadding得到的是三维的矩阵,自然不能训练,而Global层就是去掉max_length层变成二维
训练即可。
合并和padding会出现不少问题,首先是合并会丢失信息,padding又会增加噪音,而且没有主次之分。像主语这些词应该不是特别重要,而形容词是很重要的,直接合并就会稀释掉了。
而且无效计算太多了,太多的无效padding的计算,没有意义。
上面的问题可以使用RNN处理。
对于序列问题,RNN比较容易解决:


一对一,一对多,多对多,实时的多对多都可以实现。而且对于这些不定长的数据,我们只需要最后一个输入输出即可。

embedding_dim = 16
batch_size = 128
model = keras.models.Sequential()
model.add(keras.layers.Embedding(vocab_size, embedding_dim, input_length=max_length))
model.add(keras.layers.SimpleRNN(units=64, return_sequences=False))
model.add(keras.layers.Dense(64, activation='relu'))
model.add(keras.layers.Dense(1, activation='sigmoid'))
model.summary()
model.compile(optimizer = 'adam', loss = 'binary_crossentropy', metrics = ['accuracy'])
history = model.fit(train_data, train_labels, epochs=15, batch_size = batch_size, validation_split=0.2)

SimpleRNN里面的return_sequences,是否需要最后输出,每一个序列都输出。units是输出向量的长度。训练完后基本没有效果。

模型太简单了,没办法拟合数据,复杂一点,SimpleRNN是单向的循环神经网络,来个双向的循环神经网络,加多两层。

embedding_dim = 16
batch_size = 128
model = keras.models.Sequential()
model.add(keras.layers.Embedding(vocab_size, embedding_dim, input_length=max_length))
model.add(keras.layers.Bidirectional(keras.layers.SimpleRNN(units=64, return_sequences=True)))
model.add(keras.layers.Bidirectional(keras.layers.SimpleRNN(units=64, return_sequences=False)))
model.add(keras.layers.Dense(64, activation='relu'))
model.add(keras.layers.Dense(1, activation='sigmoid'))
model.summary()
model.compile(optimizer = 'adam', loss = 'binary_crossentropy', metrics = ['accuracy'])
history = model.fit(train_data, train_labels, epochs=15, batch_size = batch_size, validation_split=0.2)

然鹅又过拟合了,再来简单点,一层双向RNN就好了。添加LSTM层也一样:

model.add(keras.layers.LSTM(units=32, activation='relu'))

你可能感兴趣的:(CS231,TensorFlow)