Python 徒手实现 卷积神经网络 CNN - 知乎 (zhihu.com)
MNIST 数据集的获取和使用
在python代码中,通过下面的代码就可以获取和使用mnist数据集
import mnist
test_images = mnist.test_images()[:1000]
但是这是通过mnist模块的自动下载获取数据集的.如果网络不好,往往获取不到完成的数据集,导致后续报数据集解压缩出现异常 报错信息如下During handling of the above exception, another exception occurred
通过单步调试,可以知道mnist.test_images()
内部是通过
fname = download_file(fname, target_dir=target_dir, force=force)
fopen = gzip.open if os.path.splitext(fname)[1] == '.gz' else open
实现下载和解压的, 单步进去后查看fname的值,可以知道mnist默认的数据集下载地址. 我的环境下是C:\Users\Administrator\AppData\Local\Temp\t10k-images-idx3-ubyte.gz
. 所以对应的解决办法就是从mnist的官网MNIST handwritten digit database, Yann LeCun, Corinna Cortes and Chris Burges
手动下载所有数据集之后,覆盖到这个目录下.
交叉熵损失函数的理解
(4条消息) 一文读懂交叉熵损失函数霜溪的博客-CSDN博客交叉熵损失函数的理解
keras 学习总结
(https://keras.io/getting_started/)
1 神经网络的数据表示
张量是矩阵向任意维度的推广,张量的维度通常叫轴
零维张量
一个数字就是一个标量也叫0维张量
在numpy中的表示如下, 通过ndim可以查询张量的维度.
import numpy as np
x = np.array(12)
x.ndim
0
x
array(12)
一维张量
x = np.array([10,11,12,13])
x.ndim
1
x
array([10, 11, 12, 13])
三维张量示例
x=np.array([[[11,12,13],[14,15,16]],[[1,1,1],[2,2,2]]])
x.ndim
3
x
array([[[11, 12, 13],
[14, 15, 16]],
[[ 1, 1, 1],
[ 2, 2, 2]]])
解读张量的关键属性
x.shape
(2, 2, 3)
说明张量x是2个2行3列组成的形状. 他的维度有3个.
x.dtype
dtype('int32')
说明张量x的数据类型是int32类型
import mnist
train_images = mnist.train_images()
train_images.ndim
3
train_images.shape
(60000, 28, 28)
train_images.dtype
dtype('uint8')
上面的运行结果说明, train_images张量有3个维度, 形状是60000个28行x28行的结构. 数据类型是uint8, 也就是最大数值是255
二维张量的显示示范
借助matplotlib库进行二维张量的绘制
img = train_images[1]
import matplotlib.pyplot as plt
plt.imshow(img, cmap=plt.cm.binary)
plt.show()
张量的裁切操作
张量的操作主要目的是裁剪. 比如如下几种场景:
- 从60000个28x28的张量中选出前10个28x28的元素形成一个新的三维张量,对应的代码如下
first10Imgs = train_images[0:10]
first10Imgs.shape
(10, 28, 28)
- 从60000个28x28的张量中选出前10个28x28的元素,同时选出的10个28x28元素中,只要右下角的2x2数据
first10Imgs = train_images[0:10, 26:28,26:28]
first10Imgs.shape
(10, 2, 2)
2 张量的运算
张量的四则运算
- 形状相同两个张量的
+-*/
表示的的是两个张量逐个对应位置元素的运算
import numpy as np
a = np.array([1,2,3])
b = np.array([4,5,6])
c = a+b
c
array([5, 7, 9])
c = a*b
c
array([ 4, 10, 18])
- 形状不同时, 两个张量的
+-*/
运算时, 较小的张量会被广播,以匹配较大张量的形状.
a = np.array([1,2,3])
b = np.array(4)
a+b
array([5, 6, 7])
张量的点积
要理解张量的点积要区分三种情况. 而且要区分向量和张量的区别. 当我们说向量的时候,指的是一维张量,指的的一组数字.
下面的代码中a成为向量,没有行或者列的概念,是属于一维张量
b是一行3列的二维张量
c是3行1列的二维张量
a = np.array([1,2,3])
b = np.array([[1,2,3],])
c = np.array([[1],[2],[3]])
a.shape
(3,)
b.shape
(1, 3)
c.shape
(3, 1)
- 如果两个向量点积,结果是一个标量, 两个向量的各个对应元素进行乘积并累加, 如果两个向量的个数不一致,无法进行点积,程序会报错
a = np.array([1,3])
b = np.array([2,4,6])
c=np.dot(a, b)
Traceback (most recent call last):
File "D:\Python\Python310\lib\code.py", line 90, in runcode
exec(code, self.locals)
File "<input>", line 1, in <module>
- 如果是一个矩阵A和向量B的点积, 他的结果是一个向量C. 结果C中每个向量C的值是矩阵中一行(作为一个待操作向量和向量B的点击,生成的标量作为向量C的一个元素.
所以如果矩阵A中的一行的个数和向量B不一致,也是无法运算的
a = np.array([[1,1,1],[2,2,2]])
b = np.array([1,2,3])
c = np.dot(a,b)
c
array([ 6, 12])
a = np.array([[1,1,1,1],[2,2,2,2]])
b = np.array([1,2,3])
c = np.dot(a,b)
Traceback (most recent call last):
File "D:\Python\Python310\lib\code.py", line 90, in runcode
exec(code, self.locals)
File "<input>", line 1, in <module>
同时无法进行向量跟矩阵的点积
a = np.array([[1,1,1],[2,2,2]])
b = np.array([1,2,3])
c = np.dot(a,b)
c
array([ 6, 12])
#无法执行np.dot(b,a)
- 矩阵和矩阵的点积
这个点积规则跟大学学的矩阵点积规则一致. - 更高维的点积,暂时不理解.
张量变形运算
- reshape
张量变形指改变张量的行和列,以得到想要的形状,变形后的张量元素总个数没有变化.
比如把2行2列的张量变成4行一列
a = np.array([[1,2],[3,4]])
b = a.reshape((4,1))
b
array([[1],
[2],
[3],
[4]])
- transposition
矩阵的转置也是一种常见的变形
a = np.array([[1,2],[3,4],[5,6]])
b = a.transpose()
b
array([[1, 3, 5],
[2, 4, 6]])
深度学习的数学解释
神经网络就是由一系列的张量运算组成的, 输入张量和输出张量通过一定的数学训练后,找到一种映射关系,这种映射关系通过一层层的张量运算变换一步步实现.
对于实际的问题, 只要你针对实际问题提取的输入张量可靠, 训练的数据足够多写有代表性,那么你所针对的实际问题就有了数学上的识别模型, 这个模型可以通过你提供的数据训练出来.
3神经网络的训练引擎
神经网络的训练要利用损失的梯度方向传播的各个层的权重.
什么是导数
导数的数学含义, 对于一个函数 y = f(x). 在某个点的导数a记作d_y_d_x. 如果d_y_d_x是正数,说明微小的dx会引起dy = ad_x的正向变化(变大). 如果a是负数,说明dx的增加会导致y的减小. a的绝对值表示变化的速率.
如果我们反向求出d_L_d_w = a. 表示在当前权重w状态下,如果a是正值, 权重增加一些, 损失L就会增加,所以调节网络的时候我们要反向调节让权重减去(|a|step). step表示一个很小的比例因子.
SGD 表示随机梯度下降法
4 keras CNN 示例
神经网络的keras实现
层+损失函数定义+反向传播方式指定
- 下面的代码创建了一个全连接层, 输入是二维张量, 可以认为有784个向量, 向量的元素个数不限. 新建层的输出是一个一维张量,包含32个值.
from keras import layers
layer = layers.Dense(32, input_shape=(784,))
- keras创建网络结构的时候,先指明结构框架,然后逐个添加层. 比如下面的代码
model = models.Sequential();
model.add(layers.Dense(32,input_shape=(784,)))
model.add(layers.Dense(32))
这段代码中添加了两个全连接层,第一个dense层输入时(784,),输出是32D向量. 第二个dense创建的时候,并没有指定输入,keras会自动推导我的输入应该是上一层的输出.所以是一个32D向量输入,32D向量输出的层.
神经网络的模型是由层构成的有向无环图. 最常见的例子是一个输入分支从头到尾经过多层移动到输出. 但也存在双分支网络,多头网络,inception网络(不知道是啥网络??)
- 根据任务类型选择合适的损失函数和反向传播函数(优化器)
模型确定之后,需要编译,在编译的时候配置学习过程,如下面的代码, RMSprop是一种参数反向调整的方法.(SGD也是一种方法). loss='mse' 指定均方误差作为损失函数.
from keras import optimizers
model.compile(optimizer=optimizers.RMSprop(learning_rate=0.001),loss='mse',metrics =['accuracy'])
- 最后学习过程通过fit()方法启用
类似代码如下
model.fit(input_tensor, target_tensor, batch_size=128, epochs=10)
keras创建层的两种方式
- 利用sequential类, 如下面的代码
model = models.Sequential();
model.add(layers.Dense(32,input_shape=(784,)))
model.add(layers.Dense(32))
- 基于过程方式,利用函数式api
input_tensor = layers.Input(shape=(784,))
x = layers.Dense(32,activation ='relu')(input_tensor)
output_tensor = layers.Dense(10,activation = 'softmax')(x)
model = models.Model(inputs=input_tensor,outputs=output_tensor)
5 建模示例
5.1 二分类问题
5.1.1 数据集获取和分析
让网络学习后,可以根据电影的评论内容,判断出该评论是正面评论还是负面评论. 用到的数据集IMDB,包含了50000条评论,这些评论都有评论是正面或者负面的标签.
一样的imdb.nbz数据集在实践的过程中也发生无法下载的问题, 解决方法就是先手动下载,让后放到指定目录中
手动下载的地址(https://www.cnblogs.com/wt7018/p/13092512.html)
覆盖的路径要通过单步调试下面的代码,确认imdb的数据集加载路径才行. 我这里是C:\Users\Administrator\.keras\datasets
import keras.datasets.imdb as myimdb
(train_data, train_labels),(test_data, test_labels) = myimdb.load_data(num_words=10000)
train_data.shape
(25000,)
train_labels.shape
(25000,)
imdb在加载数据的时候. num_words=10000的意思是仅保留训练数据中前10000个最常出现的单词,低于这个频率的单词被舍弃.label中0表示负面,1表示正面.
**观察数据集train_data.shape 返回的结果是(25000,)说明train_data是25000个向量(一维张量),通过下面的代码可以查看训练集中第0条评论的样子
oneComment = train_data[0]
oneComment
[1, 14, 22, 16, 43, 530, 973, 1622, 1385, 65, 458, 4468, 66, 3941, 4, 17....]
为什么不是单词,而是数字,这是因为imdb对单词做了索引.每个不同的索引代表一个单词.
5.1.2 训练数据处理
- 对于25000个训练样本,有的样本有10个单词,有的样本有2000个单词. 个数都不一样,这种情况下创建神经网络层的时候就会出现困难. 书上的解决办法是"归一化"成10000的向量. 因为不一样的单词总个数就10000个,每个评论都抽象成一个10000D向量,某个单词出现过就在其索引位置上赋值1,否者为0. 转换代码如下
import numpy as np
def vectorize_sequences(sequences,dimension=10000):
results = np.zeros((len(sequences), dimension))
for i, sequence in enumerate(sequences):
results[i,sequence] = 1.
return results
x_train = vectorize_sequences(train_data)
x_test = vectorize_sequences(test_data)
- 把输出数据转化为25000个浮点类型数据,表明概率
y_test = np.asarray(test_labels).astype('float32')
y_train=np.asarray(train_labels).astype('float32')
train_labels
array([1, 0, 0, ..., 0, 1, 0], dtype=int64)
y_test
array([0., 1., 1., ..., 0., 0., 0.], dtype=float32)
y_test.shape
(25000,)
处理后, 输入就是25000个10000D的向量, 输出是1个25000D的向量, 每次训练的时候选一个10000D的向量,目标是一个标量. 也就是输入数据是向量,而标签是标量,这是会遇到的最简单的情况.这种问题使用带有relu激活的全连接层处理.
5.1.3 构建网络
带relu激活的Dense层表示为Dense(16,activation="relu")
这里16表示该层隐藏单元的个数. 以这个例子的背景为例,我们每次训练的输入是一个评论也就是一个10000D的向量. 我们要用16个特征维度去解析这个输入.每个维度用隐藏层中16个神经元的一个去处理. 当输入10000D的一个向量连接这个神经元的时候,他要做的最基本的数学运算是 dot(W, input)+b
, 可以认为这个卷积过程就是一个维度的分解过程, 每个维度代表的神经元有不同的权重W. 每个神经元的权重W要能够和input做卷积,所以一个神经元的W在这里必然也是一个10000D的向量. 那16个隐藏单元对应的权重矩阵W的形状就是((10000,),16)
. 隐藏单元的个数(分析的维度)并不是越多越好, 越多计算量越大,且最终学习到的识别方法有可能普适性更差(针对训练集更好了,但没有训练过的数据识别起来效果变差).
维度的解析基本运算是dot(W, input)+b
, 为了模拟一些跳跃的非线性的映射关系,加上了激活函数relu(dot(W,input)+b
针对这个case, 书上根据经验选择了如下的网络结构
from keras import models
from keras import layers
model = models.Sequential()
model.add(layers.Dense(16,activation='relu',input_shape=(10000,)))
model.add(layers.Dense(16, activation='relu'))
model.add(layers.Dense(1,activation='sigmoid'))
最后要在编译过程中选择损失函数和优化器,代码如下
model.compile(optimizer='rmsprop', loss='binary_crossentropy',metrics=['accuracy'])
5.1.4 验证模型
拆分训练集,验证集,测试集
原始的训练集合中x_train 和 y_train中有25000个评论和标签
x_train.shape
(25000, 10000)
y_train.shape
(25000,)
通过下面的代码,拆分10000个评论出来作为验证集,并开始训练.训练时训练20轮,每一轮使用512个样本.
x_varification = x_train[:10000]
x_real_train = x_train[10000:]
y_varification = y_train[:10000]
y_real_train = y_train[10000:]
history = model.fit(x_real_train, y_real_train, epochs=20,batch_size=512,validation_data= (x_varification, y_varification))
训练结果
Epoch 1/20
30/30 [==============================] - 5s 110ms/step - loss: 0.5491 - accuracy: 0.7777 - val_loss: 0.4185 - val_accuracy: 0.8619
......
[==============================] - 1s 45ms/step - loss: 0.0237 - accuracy: 0.9958 - val_loss: 0.4962 - val_accuracy: 0.8714
Epoch 19/20
30/30 [==============================] - 1s 46ms/step - loss: 0.0192 - accuracy: 0.9969 - val_loss: 0.5181 - val_accuracy: 0.8699
Epoch 20/20
30/30 [==============================] - 1s 46ms/step - loss: 0.0177 - accuracy: 0.9973 - val_loss: 0.5396 - val_accuracy: 0.8685
模型可以优化的方向
可以尝试更少或更多的隐藏层
可以尝试不同的解析维度
可以使用不同的激活函数,损失函数
通过验证集上测试的loss和精度数据,确认是否发生过拟合的情况,并优化
5.2 多分类问题
训练集是一些新闻评论,每条新闻的标签是新闻的类别,和单分类问题不一样的是,标签的类别有46个.
这个示例问题的解决方法基本和单分类的示例类似. 需要注意的地方有两个
- 多分类标签也需要进行编码转换. 转换方法就是统一用46D的向量表示标签.
- 网络的输出有46种可能,如果在设计解析维度的时候依然沿用上例的16,就有些不够了.既然有46种可能输出,那至少有46个特征维度.所以Dense层的网络节点最好比46大,书中根据经验使用了64.
- 当然输出是1D向量, 只不过0-45的不同数据.这种标签编码是整数,如果直接用1D的向量作为输出,那么要选用sparse_categorical_crossentropy作为损失函数.
如果把输出的1D向量转化为46D向量,每个向量上的标量取值不是0就是1,这种情况下使用categorical_crossentropy作为损失函数.
5.3 回归问题
5.3.1 数据集获取和分析
boston_housing数据集有506份样本. 每个样本有13个属性描述,如人均犯罪率,住宅平均房间数,公交遍历性等. 目标是房屋价格的平均值.
from keras.datasets import boston_housing
(train_data,train_targets),(test_data,test_targets) = boston_housing.load_data()
train_data.shape
(404, 13)
test_data.shape
(102, 13)
上述的代码如果下载数据集异常, 需要手动下载(https://gitee.com/polarisslk/boston_housing/blob/master/boston_housing.npz)
5.3.2 数据准备
每一个样本的13个属性, 即使是同一个属性相对于其他样本可能差异很大. 这样的数据对网络训练是不利的(为啥不利?).普遍的做法是针对每个属性(特征)做标准化.即输入的数据的每个特征减去特征的平均值,再除以标准差,这样得到的特征列都具有平均值为0,标准差为1的特点了.
数据处理代码如下. 需要注意的是训练集的数据特征标准化后,对测试集的数据特征也需要标准化. 但测试集的数据标准化,不能用测试集的统计数据!, 因为实际的测试数据集代表的是你没有提前拿到的数据,发生时,也只有发生时刻的那一份
mean =train_data.mean(axis=0)
mean.shape
(13,)
mean
array([3.74511057e+00, 1.14801980e+01, 1.11044307e+01, 6.18811881e-02,
5.57355941e-01, 6.26708168e+00, 6.90106436e+01, 3.74027079e+00,
9.44059406e+00, 4.05898515e+02, 1.84759901e+01, 3.54783168e+02,
1.27408168e+01])
train_data -= mean
std = train_data.std(axis=0)
std
array([9.22929073e+00, 2.37382770e+01, 6.80287253e+00, 2.40939633e-01,
1.17147847e-01, 7.08908627e-01, 2.79060634e+01, 2.02770050e+00,
8.68758849e+00, 1.66168506e+02, 2.19765689e+00, 9.39946015e+01,
7.24556085e+00])
train_data /=std
test_data -= mean
test_data /=std
5.3.3 构建网络
这个数据集的特点是数据较少. 书上交代如果一般训练集越少,过拟合会越严重,而较小的网络可以降低过拟合.
针对这个的网络构建和之前稍微有所不同的是,最后一层的Dense, 最后一层Dense不能使用激活函数.如果使用激活函数,最后一层的输出会是不连续的输出,而我们期望的预测价值是个连续值.
网络构建代码如下
from keras import models
from keras import layers
def build_model():
model = models.Sequential()
model.add(layers.Dense(64,activation='relu',input_shape=(train_data.shape[1],)))
model.add(layers.Dense(64,activation='relu'))
model.add(layers.Dense(1))
model.compile(optimizer='rmsprop',loss='mse', metrics=['mae'])
return model
5.3.4 K折验证
这个数据集很小,我们只划分了100个样本作为验证集. 这样验证分数会有很大的波动.这种背景下,最佳做法是:
将可用的训练数据划分为K个分区,
train_data.shape
(404, 13)
把train_data中的404张拆分成4份
书上的3.6.4小节. 后续补充.