序列化推荐算法(1):循环神经网络(RNNs)

RNNs的神经元结构

循环神经网络(Recurrent Neural Networks)是一种可以用于预测的神经网络,它可以分析诸如股票价格之类的时序数据,并告诉你何时应该买入/卖出。同时也可以在自动驾驶系统中,预测车辆的行进轨迹,从而避免发生交通事故。与其他许多神经网络不同的是,RNNs对于输入的序列长度没有要求。也就是说,你可以将长短不一的参数序列传入RNNs,比如任意一篇文章,或者任意一段音频的采样。这也是RNNs经常被应用于NLP领域的原因之一。更重要的是,RNNs基于时序数据的预测能力,使得它可以胜任包括自动编曲,文本编写等等创造性工作。

RNNs的视频已上传至我的YouTube频道,懒得看字的朋友可以直接看视频。

循环神经元

对于大部分的神经网络而言,传播方向基本都是一个方向的,也就是比较常见的前馈网络。而对于RNNs来说,它的传播方向是前后双向的。最简单的例子,就是仅由一个循环神经元(recurrent neuron)接收输入 x ( t ) x_{(t)} x(t),以及这个神经元上一时刻的输出 y ( t − 1 ) y_{(t-1)} y(t1)。如果我们将它在时间上进行延拓,就可以得到下图中展示的效果。
循环神经元在时间上的延拓

因此,生成一个由循环神经元组成的层十分简单 —— 只需要让每一个神经元在任一时刻,都接收 x t x_{t} xt的传入以及上一时刻的输出 y ( t − 1 ) y_{(t-1)} y(t1)就可以了。每一个循环神经元都有两组权重,即输入 x ( t ) x_{(t)} x(t)的权重 w x w_x wx以及前一时刻输出 y ( t − 1 ) y_{(t-1)} y(t1)的权重 w y w_y wy。对于单一循环神经元而言,任一时刻的输出 y ( t ) y_{(t)} y(t),可以被表示为:
y ( t ) = ϕ ( x t T ⋅ w x + y ( t − 1 ) T ⋅ w y + b ) y_{(t)}=\phi(x^T_t\cdot w_x+y^T_{(t-1)}\cdot w_y+b) y(t)=ϕ(xtTwx+y(t1)Twy+b)
其中 b b b为偏置项, ϕ \phi ϕ为激活函数。

循环神经层

将这样的循环神经元组成神经网络中的一个层,就可以得到一个循环神经层。即该层上一时刻的输出将会作为下一时刻输入的一部分:
循环神经层
因此对于整个层的输出可以表示为:​
Y ( t ) = ϕ ( X ( t ) ⋅ W x + Y ( t − 1 ) ⋅ W y + b ) Y_{(t)}=\phi(X_{(t)}\cdot W_x+Y_{(t-1)}\cdot W_y+b) Y(t)=ϕ(X(t)Wx+Y(t1)Wy+b)
化简为:
Y ( t ) = ϕ ( [ X ( t )   Y ( t − 1 ) ] ⋅ W + b )   w i t h   W = [ W x W y ] Y_{(t)}=\phi \left(\left[X_{(t)} \ Y_{(t-1)} \right ] \cdot W + b \right )\ with\ W=\begin{bmatrix} W_{x}\\ W_{y}\end{bmatrix} Y(t)=ϕ([X(t) Y(t1)]W+b) with W=[WxWy]
其中:

  • Y ( t ) Y_{(t)} Y(t)​是一个大小为 m × n n e u r o n s m \times n_{neurons} m×nneurons,包含 t t t时刻所有输出的矩阵。其中 m m m是该层mini-batch中所有实例的个数, n n e u r o n s n_{neurons} nneurons是神经元的个数。
  • X ( t ) X_{(t)} X(t)是一个大小为 m × n i n p u t s m \times n_{inputs} m×ninputs,包含所有实例的输入的矩阵。其中 n i n p u t s n_{inputs} ninputs是输入特征的数量。
  • W x W_x Wx​是一个大小为 n i n p u t s × n n e u r o n s n_{inputs} \times n_{neurons} ninputs×nneurons,包含输入对于当前时刻的连接权重的权重矩阵。
  • W y W_y Wy​是一个大小为 n n e u r o n s × n n e u r o n s n_{neurons} \times n_{neurons} nneurons×nneurons,包含输出对于前一时刻的连接权重的权重矩阵。
  • ​这两个权重矩阵 W x W_x Wx W y W_y Wy经常被拼接成为一个大小为 ( n i n p u t s + n n e u r o n s ) × n n e u r o n s \left ( n_{inputs}+n_{neurons} \right )\times n_{neurons} (ninputs+nneurons)×nneurons的独立的权重矩阵 W W W
  • b b b​是一个大小为 n n e u r o n s n_{neurons} nneurons,包含每个神经元的偏置项的向量。

记忆细胞

由于循环神经元在处理 t t t时刻输出时,会涉及到此前时刻的所有输入,因此也可以认为循环神经元是具有记忆的。网络中可以保存时序状态的一部分,被称为记忆细胞(memory cells)。最基础的形式就是一个循环神经元或者一个由循环神经元构成的层。

一般而言一个记忆细胞在 t t t时刻的隐状态记作 h ( t ) h_{(t)} h(t) h ( t ) h_{(t)} h(t)与当前时刻的输入以及前一时刻的隐状态相关,可表示为 h ( t ) = f ( h ( t − 1 ) , x ( t ) ) h_{(t)}=f(h_{(t-1)}, x_{(t)}) h(t)=f(h(t1),x(t))。在 t t t时刻的输出 y ( t ) y_{(t)} y(t)也是由前一时刻的隐状态和当前时刻的输入决定的。在一些复杂的情况下, h ( t ) h_{(t)} h(t) y ( t ) y_{(t)} y(t)并不相同。

RNNs的网络结构

RNNs的使用方式非常灵活。一个RNN可以通过模拟一个输入序列来生成一个输出序列,即Seq2Seq。因此可以用于预测时序序列,比如股票的价格。另一种应用方式,是输入一个序列,之后忽略掉其他输出,只保留最后一个输出,这种做法可以讲一个序列进行向量化,即Seq2Vec。比如,可以将一部电影相关的评价做成词语序列输入网络,输出电影的打分。与之相反的方式,是将其他时刻的输入全部置0,只保留第一个输入,然后让网络输出整个序列,这样它就是一个Vec2Seq网络。比如,你可以输入一个图片,然后输出关于它的文字说明。最后一种常见做法是,你可以用一个Seq2Vec网络作为encoder,之后接一个Vec2Seq网络作为decoder,这样就组成了一个delayed Seq2Seq网络。比如,你可以将一种语言翻译成另一种语言。首先,将一种语言输入到encoder部分中,使其转化为一个向量值。之后再用decoder对这个向量解码,转换为另一种语言。这种two-step模型被称为Encoder-Decoder模型,相比于普通的Seq2Seq,这种方式无需等待整句话完全输入再进行翻译。

使用TensorFlow构建简单RNN

在TensorFlow的keras中,有三种内置的RNN层,分别是SimpleRNN,GRU,以及LSTM。GRU和LSTM都是RNN的变形,SimpleRNN则是一个基础的全连接RNN。无论使用哪一种RNN层,它接收的数据格式都是(batch_size, 时间步, 特征维度)。举例来说,如果padding之后所有输入序列的长度都变为5,序列中每一个时间步都有8维特征,则直接输入到RNN层时,数据形状为(batch_size, 5, 8)。

import tensorflow as tf
from tensorflow.keras.layers import StringLookup, Embedding, SimpleRNN, Dense


class RNN4Rec(tf.keras.Model):
    def __init__(self, vocab_size, embedding_size: int = 8):
        super().__init__()
        self.embedding = Embedding(input_dim=vocab_size + 1, output_dim=embedding_size, mask_zero=True)
        self.rnn1 = SimpleRNN(8, return_sequences=True, activation='tanh')
        self.rnn2 = SimpleRNN(8, return_sequences=False, activation='tanh')
        self.dnn1 = Dense(8, activation='relu')
        self.dnn2 = Dense(vocab_size, activation='softmax')

    def call(self, inputs, training=None, mask=None):
        x = self.embedding(inputs)
        x = self.rnn1(x)
        x = self.rnn2(x)
        x = self.dnn1(x)
        x = self.dnn2(x)
        return x

这里简单地构建一个类,继承自tf.keras.Model。我们对模型的期望是,接受一系列用户的浏览过的item的序列,将这些item进行embedding之后送入RNN层,最后通过DNN预测用户下一个浏览的item是哪一个。首先要注意,SimpleRNN的参数列表中,有一项是return_sequences,当这一项为True时,SimpleRNN会为输入序列中的每一个输入值进行一次预测,并作为结果的一部分返回,也就是返回一个sequence。当RNN后面接入的依旧是RNN层时,这个参数必须设置为True;当你希望输出仅为下一个item而不是预测接下来的浏览序列时,将最后一层RNN的return_sequences设为False。

我们假设接收的数据情况大致如下:

vocab = ['item1', 'item2', 'item3', 'item4', 'item5', 'item6']

X = [
    ['item1', 'item2', 'item6', 'item3'],
    ['item1', 'item2', 'item4', 'item3'],
    ['item2', 'item3', 'item4'],
    ['item1', 'item3', 'item4', 'item5', 'item6'],
    ['item2', 'item4', 'item3', 'item5'],
    ['item5', 'item3']
]
Y = ['item4', 'item5', 'item5', 'item2', 'item1', 'item4']

在将模型输入embedding层以前,需要先将这些string类型的item序列进行padding,随后转化为int类型:

X = tf.keras.utils.pad_sequences(X, dtype=object, value='[PAD]')
string_lookup = tf.keras.layers.StringLookup(vocabulary=vocab)
X = string_lookup(X)
Y = string_lookup(Y)
data = tf.data.Dataset.from_tensor_slices((X, Y)).batch(2)

使用sparse_categorical_crossentropy作为损失函数,对模型进行训练:

model = RNN4Rec(vocab_size=len(vocab))
model.compile(optimizer='adam', loss='sparse_categorical_crossentropy')
model.fit(data, epochs=10)

训练结束后,输入测试数据让模型进行预测:

test = [
    ['item1', 'item2', 'item3'],
    ['item2', 'item4', 'item3', 'item6']
]

test = tf.keras.utils.pad_sequences(test, dtype=object, value='[PAD]')
test = string_lookup(test)

result = tf.argmax(model(test), axis=1)
  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值