【theano-windows】学习笔记十一——theano中与神经网络相关函数

时间:2022-12-14 14:10:46

前言

经过softmaxMLP的学习, 我们发现thenao.tensor中除了之前的博客【theano-windows】学习笔记五——theano中张量部分函数提到的张量的定义和基本运算外, 还有一个方法称为nnet, 如果自己实现过前面两篇博客中的代码就会发现用到了theano.tensor.nnet.sigmoidthenao.tensor.nnet.tanh这两个神经网络的激活函数, 那么就应该想了, 这个nnet是否对于在theano中实现神经网络很重要呢?所以我就跑去看了一下官网的介绍了, 感觉有必要摘抄一波.

【注】本次学习只是摘抄点感觉常用的函数, 见到陌生的操作可以再去查官方文档

进入官方对nnet的介绍的界面时, 可以发现有五种操作

  • conv:卷积神经网络相关操作
  • nnet:神经网络相关操作
  • neighbours:卷积网络中图片的处理操作
  • bn: 批归一化batch normalization
  • blocksparse: 块稀疏乘法操作(有gemvouter)

国际惯例, 参考博客

Ops related to neural networks

Theano tutorial和卷积神经网络的Theano实现

卷积相关操作

2D卷积函数

theano.tensor.nnet.conv2d(input, filters, input_shape=None, filter_shape=None, border_mode='valid', subsample=(1, 1), filter_flip=True, image_shape=None, **kwargs)#默认翻转卷积核

参数解释:

  • input: 四维张量符号变量, 维度依次代表(批大小, 输入通道, 输入行, 输入列)
  • filters: 四维张量符号变量. 维度依次代表(输出通道, 输入通道, 滤波器行, 滤波器列)
  • input_shape: 可选变量, 可以是None, 也可以是四个整数或者常量组成的元组或者列表, 代表输入的大小
  • filter_shape: 可选变量, 可以是None, 也可以是四个整数或者常量组成的元组或者列表, 用于选择优化实现, 可以指定列表中某个元素为None, 表示这个元素在编译的时候是未知的
  • border_mode: 字符串, 整型, 或者两个整型组成的元组-以下任意一种
    valid: 输出大小 inputshapeflitershape+1
    full: 输出大小 inputshape+filtershape1
    half: 使用 filterrows//2 行和 filter//colums 列对称填充输入边界,然后执行valid
    int: 使用指定的全零宽度向量填充输入的对称边界, 然后使用valid
    (int1,int2): 使用int1行和int2列填充输入的对称边界, 然后执行valid
  • subsample: 长度为2的元组, 对输出进行下采样, 也称为步长strides
  • fliter_flip: 如果为True, 就在滑动卷积窗口前翻转filter, 翻转filter的操作经常称为卷积(convolution),而且这个值经常默认为True. 如果设置为False, 那么就不翻转filter, 这个操作一般叫做互相关(cross-correlation)
  • *image_shape*: 为None或者四个整型或者常变量组成的元组或者列表, input_shape的别名, 已弃用
  • filter_dilation:长度为2的元组, 对输入进行下采样, 经常称为空洞卷积(dilation),详细看知乎的解释如何理解空洞卷积(dilated convolution)?
  • *kwargs*: 兼容性, 常被忽略

返回值: 一系列的特征图, 大小是(批大小, 输出通道, 输出行, 输出列)

theano.sandbox.cuda.fftconv.conv2d_fft(input, filters, image_shape=None, filter_shape=None, border_mode='valid', pad_last_dim=False)

简介: 是仅支持GPUnnet.conv2d是实现, 使用了傅里叶变换来实现这个操作, 也会翻转卷积核. 但是conv2d_fft不能被直接使用, 因为没有提供梯度. 仅仅支持输入的最后一个维度是偶数, 其它的维度任意, 滤波器可以具有偶数或者技术的宽度. 如果你的输入一定具有奇数宽度, 那么可以使用填充的方法来解决, 如果你不确定你的输入倒是是偶数还是技术, 千万不要随便使用填充参数pad_last_dim, 因为这个参数是无条件的加接一个维度, 可能会让偶数输入变技术, 导致一些问题的发生……..balabalabala后面一堆, 反正意思就是使用它挺麻烦的, 不如直接用nnet.conv2d

参数(在valid模式中输入必须大于滤波器)与上面的conv2d相同, 就是多了一个不一样的:

  • pad_last_dim: 无条件地对最后一个维度进行填充.返回结果时会删除这个填充的部分

3D卷积

theano.tensor.nnet.conv3d(input, filters, input_shape=None, filter_shape=None, border_mode='valid', subsample=(1, 1, 1), filter_flip=True, filter_dilation=(1, 1, 1))

参数:

  • input: 五维的符号张量. 每个维度分别代表(批大小, 输入通道, 输入深度, 输入行, 输入列)
  • filters: 五维的符号张量. 每个维度分别代表(输出通道, 输入通道, 滤波器深度, 滤波器行, 滤波器列)
  • input_shape: 输入参数大小
  • filter_shape: 滤波器参数大小
  • border_mode: 字符串, 整型, 或者两个整型组成的元组-以下任意一种
    valid: 输出大小 inputshapeflitershape+1
    full: 输出大小 inputshape+filtershape1
    half: 使用 filterrows//2 行和 filter//colums 列对称填充输入边界,然后执行valid
    int: 使用指定的全零宽度向量填充输入的对称边界, 然后使用valid
    (int1,int2,int3): 使用int1int2和’int3’行填充输入的对称边界, 然后执行valid
  • subsample: 长度为3的元组, 对输出进行下采样, 也称为步长strides
  • fliter_flip: 如果为True, 就在滑动卷积窗口前翻转filter的’x,y,z’维度, 翻转filter的操作经常称为卷积(convolution),而且这个值经常默认为True. 如果设置为False, 那么就不翻转filter, 这个操作一般叫做互相关(cross-correlation)
  • filter_dilation:长度为3的元组, 对输入进行下采样, 经常称为空洞卷积(dilation)

返回值:卷积得到的特征图, 维度分别代表(批大小, 输出通道, 输出深度, 输出行, 输出列)

theano.sandbox.cuda.fftconv.conv3d_fft(input, filters, image_shape=None, filter_shape=None, border_mode='valid', pad_last_dim=False)

通过快速傅里叶变换fft执行卷积, 仅仅支持输入的最后一维是偶数. 其它的维度可以任意, 滤波器的最后一维可以是偶数或者奇数.

最后三个维度的语义并不重要, 只要他们在输入和滤波器之间的顺序是一样的就行.比如卷积如果是在图像里面执行的, 那么可以是(duration,height,width)也可以是(height,width,duration)…….剩下的和conv2d_fft的描述一样, 就是如果你非要输入奇数维度, 请使用填充参数balabalabala………

theano.tensor.nnet.conv3d2d.conv3d(signals, filters, signals_shape=None, filters_shape=None, border_mode='valid')

简介: conv3d2d是使用conv2加上数据reshape实现的三维卷积操作, 某些情况下比conv3d快, 它在GPU上工作, 并且翻转卷积核. 包含视频的时空卷积

参数:

  • signals: 像素具有颜色通道的图像的时间序列, 形状是[Ns,Ts,C,Hs,Ws]
  • filters: 时空滤波器,维度[Nf, Tf, C, Hf, Wf]
  • signals_shape: 信号的维度
  • filter_shape: 滤波器的维度
  • border_mode: 这个与卷积相同, 有valid,full,half

注: 另一种定义signals的方法是(批, 时间, 输入通道, 行, 列) ; 另一种定义滤波器的方法(输出通道, 时间, 输入通道, 行, 列)

还有一堆其它的函数懒得贴了, 以后遇到了再说

神经网络相关操作

其实就是各种激活函数之类的:

sigmoid

sigmoid(x)=11+ex

theano.tensor.nnet.nnet.sigmoid(x)
theano.tensor.nnet.nnet.ultra_fast_sigmoid(x)
theano.tensor.nnet.nnet.hard_sigmoid(x)
''' hard_sigmoid: 1.0s ultra_fast_sigmoid: 1.3s sigmoid (with amdlibm): 2.3s sigmoid (without amdlibm): 3.7s '''

【theano-windows】学习笔记十一——theano中与神经网络相关函数

softplus

softplus(x)=loge(1+ex)

theano.tensor.nnet.nnet.softplus(x)
#神经网络中的用法经常如下
x,y,b = T.dvectors('x','y','b')
W = T.dmatrix('W')
y = T.nnet.softplus(T.dot(W,x) + b)

softsign

softsign=x1+abs(x)

theano.tensor.nnet.nnet.softsign(x)

softmax

softmaxij(x)=exijkexik

有一个小技巧就是这个softmax的计算是稳定的, 因为它采用了如下计算过程:

e_x = exp(x - x.max(axis=1, keepdims=True))
out = e_x / e_x.sum(axis=1, keepdims=True)

也就是说先减去了一个最大值, 随后才采用softmax的标准式子计算属于每个类别的概率(突然想到, 如果之前层用sigmoid激活, 是不是softmax就不需要减去最大值了么?因为sigmoid使得 x{0,1} ,而使用Relu的话, 最好还是减一下, 但是总而言之, 无论是caffe还是theano都进行了减最大值操作). 为什么要减?因为如果上一层输出 x 很大, 那么编译器就会发生上溢出( e1000 )或者下溢出( e1000 ), 那么减去最大值以后肯定不会上溢出了, 即使发生下溢出, 也会由于取对数而解决此问题. 详细原因请戳这里1,这里2

x,y,b = T.dvectors('x','y','b')
W = T.dmatrix('W')
y = T.nnet.softmax(T.dot(W,x) + b)

Relu

theano.tensor.nnet.relu(x, alpha=0)

alpha是负值输入的斜率,属于0-1之间, 默认为0(标准的Relu), 如果是1的话, 激活函数就成线性激活了, 而其它的数就是传说中的Leaky Relu

因为这个函数比较重要, 所以扩展一下说明它的三种形式Relu,LeakyRelu,PRelu,摘自[Caffe]:关于ReLU、LeakyReLU 、PReLU layer

  • Relu

    forward activation:f(x)=max(0,x)backward gradient:Ex={0, if x0Ey,if x>0

  • Leaky Relu

    forward activation:f(x)=max(0,x)+negativeslopmin(0,x)backward gradient:Ex=vEy, if x0Ex, if x>0

  • Parametric Relu

    forward activation:f(xi)=max(0,xi)+aimin(0,xi)backward activation:for xi:Exi=aiEyi, if xi0Eyi, if xi>0for ai:Eai={xixiEyi, if xi00, if xi>0

binary_crossentropy

crossentropy(t,o)=(tlog(o)+(1t)log(1o))

#theano.tensor.nnet.nnet.binary_crossentropy(output, target)
x, y, b, c = T.dvectors('x', 'y', 'b', 'c')
W = T.dmatrix('W')
V = T.dmatrix('V')
h = T.nnet.sigmoid(T.dot(W, x) + b)
x_recons = T.nnet.sigmoid(T.dot(V, h) + c)
recon_cost = T.nnet.binary_crossentropy(x_recons, x).mean()

categorical_crossentropy

返回近似分布和真实分布的交叉熵, 如果编码方案是基于给定的概率分布q而非真实分布p,那么两个概率分布之间的交叉熵衡量的就是从一系列的可能性中区分一个时间所需要的平均编码长度:

H(p,q)=xp(x)log(q(x))

y = T.nnet.softmax(T.dot(W, x) + b)
cost = T.nnet.categorical_crossentropy(y, o)
# o is either the above-mentioned 1-of-N vector or 2D tensor

h_softmax

实现的是两层层级softmax, 如果输出的数目很重要的时候, 此函数就是softmax的另一个可选项

theano.tensor.nnet.h_softmax(x, batch_size, n_outputs, n_classes, n_outputs_per_class, W1, b1, W2, b2, target=None)

参数说明:

  • x:批大小*特征数, 也就是双层层级softmax的小批输入
  • batch_size: 输入x的小批大小
  • n_outputs: 输出的个数
  • n_classes:双层层级softmax的类别数, 对应第一个softmax的输出数目
  • n_outputs_per_class:每一类的输出个数
  • W1: 输入x的特征数*类别数, 第一个softmax的权重矩阵, 将输入x映射到类别概率
  • b1: 维度就是类别数, 第一个softmax的偏置
  • W2:(类别数*输入x的特征总数*n_outputs_per_class, 第二个softmax的权重矩阵
  • b2: 维度是n_classes*n_outputs_per_class,第二个softmax的偏置
  • target:维度是(batch_size,)或者(batch_size,1),对于每一个输入计算对应的目标输出, 如果为None, 那么就计算每个输入对应的所有输出

返回值: 取决于target, 它有两种不同大小的输出. 如果target没有指定(None)的时候, 那么所有的输出就被计算出来, 而且返回值大小为(batch_size,n_outputs), 反之, 当target指定以后, 仅仅有对应的输出被返回, 大小是(batch_size,1)

注意: n_output_per_classn_classes的乘积必须大于或者等于n_outputs,如果严格大于, 那么相关输出将会被忽略.n_outputs_per_classn_classes必须与W1,b1,W2b2的对应维度相同。计算效率最高的时候是当n_outputs_per_class和n_classes等于n_outputs的平方根时

【PS】感觉意思就是说返回的是(输入样本*预测出的标签)或者是(输入样本*所有可能标签的预测值), 到底是不是, 以后遇到再说.

卷积网络中处理图像的操作

images2neibs

#常用于池化操作
theano.tensor.nnet.neighbours.images2neibs(ten4, neib_shape, neib_step=None, mode='valid')

就不翻译文档了, 函数功能大概就是讲输入图像ten4按照大小为neib_shape的块滑动从图像中取块, 并将每一块拉成一个向量存着

参数:

  • ten4: 输入图像, 四维的, (dim1,dim2,row,col)前两个维度可以是通道和批
  • neib_shape: 包含两个值, 滑动窗口的高宽
  • neib_step:滑动的时候跳过的间隔, 类似于卷积的步长, 但是跳过的应该是块, 也就是说如果值为1, 那么每次取得的块是相邻但是不想交的, 比如(4,4)就是取行第1-4,5-8,9-12….的块, 而卷积是取1-4,2-5,3-6的块,这也就是为什么我们搜这个函数, 在谷歌上展示的都是实现池化操作的原因
  • mode:valid需要输入是池化因子的倍数,ignore_borders: 如果不是倍数, 就忽视边界

看个例子:

# Defining variables
images = T.tensor4('images',dtype= theano.config.floatX)
neibs = T.nnet.neighbours.images2neibs(images, neib_shape=(5, 5))

# Constructing theano function
window_function = theano.function([images], neibs)

# Input tensor (one image 10x10)
im_val = np.arange(100.,dtype=theano.config.floatX).reshape((1, 1, 10, 10))

# Function application
neibs_val = window_function(im_val)

print im_val
print neibs_val

可以发现原始图像为

[[[[  0.   1.   2.   3.   4.   5.   6.   7.   8.   9.]
   [ 10.  11.  12.  13.  14.  15.  16.  17.  18.  19.]
   [ 20.  21.  22.  23.  24.  25.  26.  27.  28.  29.]
   [ 30.  31.  32.  33.  34.  35.  36.  37.  38.  39.]
   [ 40.  41.  42.  43.  44.  45.  46.  47.  48.  49.]
   [ 50.  51.  52.  53.  54.  55.  56.  57.  58.  59.]
   [ 60.  61.  62.  63.  64.  65.  66.  67.  68.  69.]
   [ 70.  71.  72.  73.  74.  75.  76.  77.  78.  79.]
   [ 80.  81.  82.  83.  84.  85.  86.  87.  88.  89.]
   [ 90.  91.  92.  93.  94.  95.  96.  97.  98.  99.]]]]

而用[5*5]的块滑动取值以后变成了

[[  0.   1.   2.   3.   4.  10.  11.  12.  13.  14.  20.  21.  22.  23.
   24.  30.  31.  32.  33.  34.  40.  41.  42.  43.  44.]
 [  5.   6.   7.   8.   9.  15.  16.  17.  18.  19.  25.  26.  27.  28.
   29.  35.  36.  37.  38.  39.  45.  46.  47.  48.  49.]
 [ 50.  51.  52.  53.  54.  60.  61.  62.  63.  64.  70.  71.  72.  73.
   74.  80.  81.  82.  83.  84.  90.  91.  92.  93.  94.]
 [ 55.  56.  57.  58.  59.  65.  66.  67.  68.  69.  75.  76.  77.  78.
   79.  85.  86.  87.  88.  89.  95.  96.  97.  98.  99.]]

neibs2images

就是images2neibs的逆操作

theano.tensor.nnet.neighbours.neibs2images(neibs, neib_shape, original_shape, mode='valid')

可以把上面的neibs_val还原成原始图片矩阵

im_new = T.nnet.neighbours.neibs2images(neibs, (5, 5), im_val.shape)
# Theano function definition
inv_window = theano.function([neibs], im_new)
# Function application
im_new_val = inv_window(neibs_val)
print im_new_val

输出

[[[[  0.   1.   2.   3.   4.   5.   6.   7.   8.   9.]
   [ 10.  11.  12.  13.  14.  15.  16.  17.  18.  19.]
   [ 20.  21.  22.  23.  24.  25.  26.  27.  28.  29.]
   [ 30.  31.  32.  33.  34.  35.  36.  37.  38.  39.]
   [ 40.  41.  42.  43.  44.  45.  46.  47.  48.  49.]
   [ 50.  51.  52.  53.  54.  55.  56.  57.  58.  59.]
   [ 60.  61.  62.  63.  64.  65.  66.  67.  68.  69.]
   [ 70.  71.  72.  73.  74.  75.  76.  77.  78.  79.]
   [ 80.  81.  82.  83.  84.  85.  86.  87.  88.  89.]
   [ 90.  91.  92.  93.  94.  95.  96.  97.  98.  99.]]]]

Batch Normalization

batch_normalization_train

theano.tensor.nnet.bn.batch_normalization_train(inputs, gamma, beta, axes='per-activation', epsilon=0.0001, running_average_factor=0.1, running_mean=None, running_var=None)

简介: 对于给定输入计算批归一化, 使用输入的均值和方差,下图摘自论文笔记-Batch Normalization

【theano-windows】学习笔记十一——theano中与神经网络相关函数
【theano-windows】学习笔记十一——theano中与神经网络相关函数
参数

  • axes: 输入的需要沿着哪个轴被归一化,取值为‘per-activation’, ‘spatial’ or a tuple of ints, 好像这个取值还会影响最终结果的好坏, 详细请戳这里
  • gamma: 缩放因子
  • beta: 偏置
  • epsilon:批归一化公式中的 ϵ ,最小 1e5
  • running_average_factor: 更新运行时均值和方差使用的更新因子
  • running_mean: running_mean * (1 - r_a_factor) + batch mean * r_a_factor
  • running_var:running_var * (1 - r_a_factor) + (m / (m - 1)) * batch var * r_a_factor

返回值:

  • out: 批归一化后的输入
  • mean: 沿着归一化轴的输入的均值
  • invstd: 沿着归一化轴的输入的逆标准差
  • new_running_mean: 运行时的均值
  • new_running_var: 运行时方差

这个函数的操作等价于:

# for per-activation normalization
axes = (0,)
# for spatial normalization
axes = (0,) + tuple(range(2, inputs.ndim))
mean = inputs.mean(axes, keepdims=True)
var = inputs.var(axes, keepdims=True)
invstd = T.inv(T.sqrt(var + epsilon))
out = (inputs - mean) * gamma * invstd + beta

m = T.cast(T.prod(inputs.shape) / T.prod(mean.shape), 'float32')
running_mean = running_mean * (1 - running_average_factor) + \
               mean * running_average_factor
running_var = running_var * (1 - running_average_factor) + \
              (m / (m - 1)) * var * running_average_factor

batch_normalization_test

theano.tensor.nnet.bn.batch_normalization_test(inputs, gamma, beta, mean, var, axes='per-activation', epsilon=0.0001)

对给定的输出使用给定的均值和方差进行批归一化, 参数就不说了, 输出是被归一化的输入. 这个操作的等价代码如下:

# for per-activation normalization
axes = (0,)
# for spatial normalization
axes = (0,) + tuple(range(2, inputs.ndim))
gamma, beta, mean, var = (T.addbroadcast(t, *axes)
                          for t in (gamma, beta, mean, var))
out = (inputs - mean) * gamma / T.sqrt(var + epsilon) + beta

batch_normalization

theano.tensor.nnet.bn.batch_normalization(inputs, gamma, beta, mean, std, mode='low_mem')

与上面的区别就是这个函数虽然使用了GPU, 但是没有使用cuDNN优化,还有其它的暂时也不清楚, 以后遇到再看看具体用法

其它

SparseBlockGemv

好像就是一个图像中取出某一块, 然后将它乘以一个矩阵之类并返回一条向量的操作, 调用方法不太懂, 是个类方法

class theano.tensor.nnet.blocksparse.SparseBlockGemv(inplace=False)

操作类似于这样

for b in range(batch_size):
    for j in range(o.shape[1]):
        for i in range(h.shape[1]):
            o[b, j, :] += numpy.dot(h[b, i], W[iIdx[b, i], oIdx[b, j]])

这个图解释的很清楚

【theano-windows】学习笔记十一——theano中与神经网络相关函数

也就是按照outputIdxinputIdxW进行索引, 然后与h对应位置相乘,而且也举了个例子当outputIdx=1也就是j=3时候的计算方法

make_node(o, W, h, inputIdx, outputIdx)

简介: 计算指定一条向量和矩阵的点乘.

参数:

  • o: 输出向量, 大小为(batch*, oWin, *oSize)
  • W: 权重矩阵, 大小为(iBlocks*, oBlocks, iSize, *oSize)
  • h: 低层的输入, 大小为(batch*, iWin, *iSize)
  • inputIdx: 输入块的索引, 大小为(batch*, *iWin)
  • outputIdx: 输出块的索引, 大小为 (batch*, *oWin)

返回值: dot(W[i, j], h[i]) + o[j], 大小为(batch, oWin, oSize)

注意:

  • batch:是批大小
  • iSize: 是每个输入块的大小
  • iWin: 是作为输入的块的数目, 这些块被inputIdx指定
  • oBlock: 是可能的输出块的数目
  • oSize: 是输出块的大小
  • oWin是输出块的数目, 可以被计算出来,每块是通过outputIdx计算的

SparseBlockOuter

class theano.tensor.nnet.blocksparse.SparseBlockOuter(inplace=False)

计算两组向量的外积, 使用结果更新整个矩阵, 操作类似于

for b in range(batch_size):
    o[xIdx[b, i], yIdx[b, j]] += (alpha * outer(x[b, i], y[b, j]))
make_node(o, x, y, xIdx, yIdx, alpha=None)

输入参数:

  • o: (xBlocks*, yBlocks, xSize, *ySize)
  • x: (batch*, xWin, *xSize)
  • y: (batch*, yWin, *ySize)
  • xIdx: x块的索引(batch*, *iWin)
  • yIdx: y块的索引(batch*, *oWin)

返回值:outer(x[i], y[j]) + o[i, j], 大小是(xBlocks, yBlocks, xSize, ySize)

注意

  • batch是批大小
  • xBlocks是x中的块的总数
  • xSize是这些x块中的每一个的大小。
  • xWin是将用作x的块的数量,将使用哪些块在xIdx中指定。
  • yBlocks是数字或可能的y块。
  • ySize是这些y块中的每一个的大小。
  • yWin是实际计算的y块的数量,将在yIdx中指定要计算的块。

sparse_block_dot

theano.tensor.nnet.blocksparse.sparse_block_dot(W, h, inputIdx, b, outputIdx)

简介: 计算指定的向量和矩阵的点积(加上偏差)

参数:

  • W: 权重矩阵, 大小为(iBlocks*, oBlocks, iSize, *oSize)
  • h: 低层输入, 大小为(batch*, iWin, *iSize)
  • inputIdx: 输入块的索引, 大小为 (batch*, *iWin)
  • b: 偏置向量, 大小为 (oBlocks*, *oSize)
  • outputIdx: 输出索引, 大小为 (batch*, *oWin)

返回值: dot(W[i, j], h[i]) + b[j], 大小是(batch, oWin, oSize)

注意

  • batch是批次大小。
  • iBlocks是输入(来自较低层)中的块的总数。
  • iSize是每个输入块的大小。
  • iWin是将用作输入的块的数量,哪些块将在inputIdx中指定,
  • oBlocks是数字或可能的输出块。
  • oSize是每个输出块的大小。
  • oWin1是实际计算的输出块数,将在outputIdx中指定要计算哪些块。

后记

这一次学习比较枯燥, 看了很多函数都不知道怎么用, 但是知道了里面有很多卷积操作, 提供了一堆激活函数和损失函数及其变种, 还可以实现池化操作, 具有batch normalize的实现, 还能指定对那一块进行乘法操作, 然后更新整个矩阵. 厉害了我的theano!!