程序代码见: https://github.com/garstka/char-rnn-java

1. 模型图示

从模型中可以看出上一时刻输出对下一时刻的影响,要清楚知道是如何影响的,还得看 3.2.3.1 节的核心代码。

2. 相关技术

RNN 是序列分析的方法。

2.1 循环/递归

Recursive 或 recurrent,我现在傻傻分不清楚。其意思就是,把 ( t − 1 ) (t-1) (t1) 时刻的中间输出作为 t t t 时刻的一部分输入,表达时序关系。

2.2 双向

由于有些序列并非时序,如 NLP 的句子。这样,后面的数据对前面的产生了影响,因此需要双向 RNN.

3. 代码分析

程序代码见: https://github.com/garstka/char-rnn-java
为了学习它, 我又来逐个方法来分析.
这个程序的优势在于:

  1. 它不依赖于其它的任何包, 因此对于初学者很友好.
  2. 它使用了比较多的文件, 每个文件仅负责一个类, 这也符合我个人编程习惯.

3.1 math包

该包负责一些数学运算.

3.1.1 class Utils

按理说这里应该提供很多的静态方法, 但是仅有 3 个.

对一个二维数组进行深度拷贝. 这个数组并不需要是规则的 $m \times n$.
deepCopyOf(double[][] src): 
获取行数.
arrayRows(double[][] array)
获取列数, 如果不同行的列数不同, 就会抛出异常.
arrayCols(double[][] array)

3.1.2 class Math

看起来要写很多东西, 却只有一点点.

// 两个数是否足够接近 (如1e-6).
close(double a, double b)
// 计算一个矩阵的 softmax.
softmax(Matrix yAtt)
// 矩阵先除以 temperature 再计算softmax.
softmax(Matrix yAtt, double temperature)

3.1.3 class Matrix

int M: 行数
int N: 列数
double[][] data: 数据

500行代码, 算多的了. 涵盖了重要的矩阵操作. 甚至包括了向量的操作.

Matrix(int M, double data[]) // 把一维数组折叠成二维矩阵.
zerosLike(Matrix other) // 生成一个新矩阵, 与原矩阵相同大小, 但元素全为 0.
oneHot(int k, int i) // 生成一个高度为 1, 宽度为 k 的矩阵, 第 i 个分量为 1, 其它为 0.
// 矩阵相乘, a 的大小为 $m \times n$, b 的大中为 $n \times k$, 则结果大小为 $m \times k$. 
// 如果 a 与 b 为一维列向量, 则将 b 转置. 如果 b 为一维行向量, 则 b.N = a.N, 也将 b 转置. 
// 否则就无法相乘.
dot(Matrix a, Matrix b) 
add(double x) // 所有元素加 x.
add(Matrix other) // 支持相同大小矩阵相加, 行向量与列向量相加.
mul(double x) // 所有元素乘以 x.
mul(Matrix other) // 逐个数据点相乘, 这才是点乘.
div(double x) // 所有元素除以 x.
div(Matrix other) // 逐个数据点相除. 支持行向量与列向量的运算, 需要长度相同.
exp() // 逐个数据点求指数. 不明白为啥突然使用 for (double[] row : data) 这种风骚操作,
// 而不使用  for (int i = 0; i < M; i ++) 这种朴素写法.
clip(double x_a, double x_b) // 把越过 [x_a, x_b] 区间的数改为相应的边界值.
apply(UnaryOperator<Double> f) // 抽象的单变量函数. 没玩过.
prod() // 所有元素相乘.
oneHotIndex() // 获得 one-hot 编码的下标.  

3.1.4 class Random

生成不同的随机矩阵.

3.2 net 包

基础网络都在这里了.

3.2.1 class RNN

抽象类. 所有 RNN 的祖先.
abstract boolean isInitialized()
abstract int getVocabularySize()

3.2.2 class SimpleRNN

抽象类. 仅多定义了
abstract initialize(int vocabularySize)

3.2.3 class RNNLayer

double learningRate; // 学习率, 反向传播时使用
// Dimensions
int inputSize; // input vector size
int hiddenSize; // hidden state size
int outputSize; // input vector size

// Defaults
static final int defaultInputSize = 50;
static final int defaultHiddenSize = 100;
static final int defaultOutputSize = 50;
static final double defaultLearningRate = 0.1;

// Network state
Matrix Wxh; // input layer weights, hiddenSize 行,  inputSize 列
Matrix Whh; // hidden layer weights, hiddenSize 行, hiddenSize 列
Matrix Why; // output layer weights, outputSize 行, hiddenSize 列
Matrix bh; // hidden bias, 1行, hiddenSize 列
Matrix by; // output bias, 1行, outputSize列

Matrix h; // last hidden state

// Training state
Matrix gWxh; // gradient descent params: input layer
Matrix gWhh; // gradient descent params: hidden layer
Matrix gWhy; // gradient descent params: output layer
Matrix gbh; // gradient descent params: hidden bias
Matrix gby; // gradient descent params: output bias

Matrix[] xAt; // input vectors through time
Matrix[] hAt; // hidden state vectors through time
Matrix[] yAt; // unnormalized output probability vectors through time
Matrix[] pAt; // normalized output probability vectors through time
Matrix[] dxAt; // output gradient from a backwards pass

int lastSequenceLength; // Number of steps in the last forward pass
					// (must match the steps for the backward pass)

这是一个很重要的类. 对一个层进行了定义. 需要着重分析涉及的参数个数.
ixTox(int ix[]): 将 ix 的每个分量转换为一个 one-hot 向量, 返回一个向量 (1 × \times × N 矩阵) 的数组.

3.2.3.1 forward(Matrix x[])
hAt[0] = new Matrix(h); // copy the current state
for (int t = 1; t < lastSequenceLength + 1; ++t) {
    // find the new hidden state
    hAt[t] = (Matrix.dot(Wxh, xAt[t]).add(Matrix.dot(Whh, hAt[t - 1]))
        .add(bh)).tanh();
    // find unnormalized output probabilities
	yAt[t] = Matrix.dot(Why, hAt[t]).add(by);
	// normalize output probabilities
	pAt[t] = Math.softmax(yAt[t]);
}
/* Update the hidden state */
h = hAt[lastSequenceLength];

前馈. 输入是矩阵组成的序列. 如果是输入层, 每个矩阵是一个 one-hot 向量. 从这里看出:

  1. 输入接受的是整个序列, 而不是每次一个分量. 序列后面分量的处理依赖于前面.
  2. Layer 内部只有一个输入端, 一个隐藏层, 一个输出端. 输入的端口数与当前序列长度 (x.length) 没有关系, 为预先定义, 如 one-hot 编码的向量维度. 输入端到隐藏层, 使用了当前时刻 t 加权和, 以及上一时刻 t -1 加权和, 再求和之后用了激活函数. 这个阶段没有使用偏移量.
  3. 隐藏层到输出端又使用了加权和与 softmax 归一化函数.
  4. 根据第1句与最后1句, 上一个序列结束时的输出, 会给下一个序列开始时使用. 这个有什么道理? 难道是前一序列获得的状态比随机初始化的状态好?
  5. 只关心 t 与 t - 1 时刻的数据, 而未关心 t 在序列中的位置. 神奇哦.
  6. 每个输入向量对应于一个输出向量. 这个应用于 NLP 可以,但时序不一定合适, 特别是单时序.

3.2.3.2 backward(Matrix dy[])

4. 讨论

本节列出一些我自己的体会.

4.1 关于 Layer

Layer 在不同的神经网络中有不同的涵义.

4.1.1 ANN的Layer

在基础神经网络中, 节点 存储与当前对象相关的数据, 所以为临时数据; 存储权重数据, 它们是网络训练的结果. 除输入层外, 每层包括4个要素:

  1. 上一层的输出值, 即本层的输入值.
  2. 上一层与本层连接边, 这些边上面的权重至关重要.
  3. 本层节点, 负责加权和、激活函数. 每个节点仅存储一个实型值.
  4. 本层输出, 丢出去就行了, 让下一层来管. 输入层仅包含这个要素.
    我刚学的时候有一个误解, 以为节点组成了 Layer, 现在看来边也是 Layer 的重要部分.

4.1.2 CNN的Layer

除输入层外, 每层包括5个要素:

  1. 上一层的输出值, 即本层的输入值.
  2. 卷积核, 其大小一般由人为指定, 其值存储网络训练的结果.
  3. 卷积层节点, 每个节点保存一个卷积结果 (一张图), k 卷积核则对应于 k 个节点, 相当于提取了输入的不同特征. 卷积层相当于一个子层.
  4. 采样层节点, 它也是一个子层, 通过采样 (池化) 获得新的图.
  5. 本层输出, 也是丢出去就行了.

4.1.3 RNN的Layer

每个Layer已经可以当作独立的网络, 它有 1 个输入子层, 1 个子隐藏层, 1个输出子层. 每个 Layer 包括5个要素:

  1. 上一层的输出值, 即本层的输入值.
  2. 输入子层与隐藏层的连边 (权重矩阵1), 隐藏层与 t - 1 时刻隐藏层输出的连边 (权重矩阵2). 后者是 RNN 的关键.
  3. 隐藏层节点, 负责两个加权和, 以及相应的激活函数. 见 2.3.1 节代码第 4 行.
  4. 隐藏层与输出层的连边 (权重矩阵3).
  5. 本层输出.

5. 小结

继续努力 ~

Logo

瓜分20万奖金 获得内推名额 丰厚实物奖励 易参与易上手

更多推荐