共计 7646 个字符,预计需要花费 20 分钟才能阅读完成。
本文主要是参考了http://blog.echen.me/2011/07/18/introduction-to-restricted-boltzmann-machines/ 这篇文章,这个文章从二分因子的角度去理解,真的是讲得清新脱俗。
背景:假设你要求一群用户从0-100分来给一组电影打分。在经典的因子分析中,你可以尝试依据一组隐藏因子来解释每部电影及用户。例如,像星球大战和指环王这类电影与“科幻小说和魔幻”这类隐藏因子可能强相关,而喜欢瓦力和玩具总动员的用户与“皮克斯动画工作室”这一隐藏因子可能强相关。
从到这的分析可以大致看出,我们需要是发现背后的因子与当前我们实际观察的数据之间的关系。
RBM可以理解为一种二值化的因子分析法(这是对它的一种理解方式,当然还有其他的理解方式及用途,原文作者主要采用二值化因子分析法去解释并实现)。与以往让用户采用一个连续的分数段为电影打分不同的是,这里只是告诉你他们“喜欢”还是“不喜欢”一部电影(1或0),之后RBM将会尝试找到之所以这样去选择电影的隐藏因子。
例如,假设我们有一组共六部电影(哈利·波特、阿凡达、指环王3、角斗士、泰坦尼克号和星梦泪痕)并且让用户告诉我们他们想看哪些。如果我们想学到两个隐藏单元潜在的电影偏好—比如,在我们六部电影里呈现出了两个自然分组:“科幻小说/魔幻”组(包括哈利·波特、阿凡达和指环王3)以及“奥斯卡获得者”组(包括指环王、角斗士和泰坦尼克号),所以我们可能希望我们的隐藏单元将会与这些分组相对应—那么,RBM可能看起来像这样:
假设我们的两个隐藏单元确实与“科幻小说/魔幻”及“奥斯卡获得者”相对应。
- 如果Alice告诉我们她关于六部电影的二进制偏好值(可视层),我们就可以去问我们的RBM:与她偏好相对应的哪个隐藏单元会被激活(1或0)(如:要求RBM依据隐藏因子去解释她的偏好)。所以可视层的六部电影向隐藏单元发送消息,以更新隐藏层。(注意:即使Alice已经声称她想看哈利·波特、阿凡达和指环王3,也不能保证代表“科幻小说/魔幻”的隐藏层会被置1,但是只有那样才会有很大的可能性被置1。这也说得通:在现实世界中,因为Alice想看这三部电影通常使我们非常确信她喜欢“科幻小说/魔幻”类型的,但是,还是有可能因为其他原因使她做出这样的选择。因此,RBM允许我们在这个混乱真实的世界为人生成模型)
- 反过来,如果我们知道某人喜欢“科幻小说/魔幻”(此时“科幻小说/魔幻”隐藏单元被置1),我们便可以要求这个隐藏单元置1的电影单元RBM(比如,要求它生成一组电影推荐)。所以,隐藏单元向电影单元(可视层)发消息,告诉他们去更新他们的状态。(请再次注意,“科幻小说/魔幻”单元被置1不能保证我们总会推荐这三部电影:哈利·波特、阿凡达和指环王3。因为,你想想,并不是每个喜欢科幻小说的人都喜欢阿凡达)
上面举得这个例子相对来说比较简单,其实在推荐系统中的用户比这个复杂一些,也就是用户的兴趣会点亮多个隐层,也就是反映用户兴趣的多样性。
RBM的运作方式
可视层的神经元用\(x_i\)表示,隐藏层神经元用\(x_j\)表示,它们之间的权重用\(w_ij\)表示,可视层神经元个数为m,隐藏层神经元个数为n。当给定可视层状态后,用下式更新隐藏层的状态。
\begin{equation}net_j=\sum_{i=0}^m{x_i{w_{ij}}}\end{equation}
\(x_0\)是偏置单元,总为1
\begin{equation}prob(j)=sigmoid(net_j)=\frac{1}{1+e^{-net_j}}\end{equation}
\(x_j\)以概率prob(j)取1,以概率1−prob(j)取0。
sigmoid函数关于(0,0.5)这一点中心对称,x为正时sigmoid(x)>0.5,x→∞时sigmoid(x)→1。
根据隐藏层求可视层方式雷同,就不写公式了。
对于推荐系统来说,我们知道用户购买了哪些商品,将对应的可视层神经元置为1,其他置为0,求出隐藏层状态,由隐藏层再返回来求可视层状态,这个时候可视层哪些神经元为1我们就把相应有商品推荐给用户。
权重学习方法
训练RBM网络就是训练权重\(w_{ij}\)。首先随机初始化\(w_{ij}\),然后每一次拿一个样本(即可视层是已知的)经历下面的步骤。
由可视层的\(x_i\)算出隐藏层的\(x_j\),令\(w_{ij}\)的正向梯度为
positive(wij)=xi∗xj
由隐藏层\(x_j\)再来反向计算\(x′_i\),注意此时算出的\(x′_i\)跟原先的\(x_i\)已经不一样了,令\(w_{ij}\)的负向梯度为
negative(wij)=x′i∗xj
更新权重
w_{ij}=w_{ij}+α∗(positive(w_{ij})−negative(w_{ij}))
我们不去深究为什么正向梯度和负向梯度是这样一个公式。上述学习方式叫对比散度(contrastive divergence)法。
循环拿样本去训练网络,不停迭代,直到收敛(即\(x′_i\)和\(x_i\)很接近)。
首先,我使用一些造的数据来训练RBM:
- Alice: (Harry Potter = 1, Avatar = 1, LOTR 3 = 1, Gladiator = 0, Titanic = 0, Glitter = 0). Big SF/fantasy fan(科幻超级迷妹).
- Bob: (Harry Potter = 1, Avatar = 0, LOTR 3 = 1, Gladiator = 0, Titanic = 0, Glitter = 0). SF/fantasy fan, but doesn’t like Avatar(科幻迷,但不喜欢阿凡达).
- Carol: (Harry Potter = 1, Avatar = 1, LOTR 3 = 1, Gladiator = 0, Titanic = 0, Glitter = 0). Big SF/fantasy fan(科幻超级迷).
- David: (Harry Potter = 0, Avatar = 0, LOTR 3 = 1, Gladiator = 1, Titanic = 1, Glitter = 0). Big Oscar winners fan(奥斯卡超级迷).
- Eric: (Harry Potter = 0, Avatar = 0, LOTR 3 = 1, Gladiator = 1, Titanic = 0, Glitter = 0). Oscar winners fan, except for Titanic(奥斯卡迷,但不喜欢泰坦尼克号(译者注:原文此处Titanic = 1有误)).
- Fred: (Harry Potter = 0, Avatar = 0, LOTR 3 = 1, Gladiator = 1, Titanic = 1, Glitter = 0). Big Oscar winners fan(奥斯卡超级迷).
该网络学习到了以下权重:
请注意,第一个隐藏单元看起来对应“奥斯卡获得者”,第二个隐藏单元看起来对应“科幻小说/魔幻”,跟我们预期一致。
如果给RBM一个新的用户输入会发生什么呢?(可视层到隐藏层)比如George, 他的偏好是(Harry Potter = 0, Avatar = 0, LOTR 3 = 0, Gladiator = 1, Titanic = 1, Glitter = 0) ,结果是“奥斯卡获得者”对应的隐藏单元被置1(而不是“科幻小说/魔幻”),正确地猜出乔治可能更喜欢“奥斯卡获得者”类型的电影。
如果我们只激活“科幻小说/魔幻”对应的单元,并且让RBM训练一段时间会发生什么呢?(隐藏层到可视层)在我的试验中,有三次哈利·波特、阿凡达和指环王3被置1;有一次阿凡达和指环王3被置1,而哈利波特没有;有两次,哈利·波特和指环王3被置1,而阿凡达没有。注意,给予我们的训练集,这些生成的偏好的确匹配了我们觉得真正的科幻迷想要看的电影。
代码实践
# -*- coding: utf-8 -*-
# @Time : 2019/3/22 下午5:07
# @Author : zhusimaji
# @File : rbm.py
# @Software: PyCharm
import numpy as np
class RBM(object):
def __init__(self, num_visible, num_hidden, learn_rate=0.1, learn_batch=1000):
self.num_visible = num_visible # 可视层神经元个数
self.num_hidden = num_hidden # 隐藏层神经元个数
self.learn_rate = learn_rate # 学习率
self.learn_batch = learn_batch # 每次根据多少样本进行学习
'''初始化连接权重'''
self.weights = 0.1 * \
np.random.randn(self.num_visible,
self.num_hidden) # 依据0.1倍的标准正太分布随机生成权重
# 第一行插入全0,即偏置和隐藏层的权重初始化为0
self.weights = np.insert(self.weights, 0, 0, axis=0)
# 第一列插入全0,即偏置和可视层的权重初始化为0
self.weights = np.insert(self.weights, 0, 0, axis=1)
def _logistic(self, x):
'''直接使用1.0 / (1.0 + np.exp(-x))容易发警告“RuntimeWarning: overflowencountered in exp”,
转换成如下等价形式后算法会更稳定
'''
return 0.5 * (1 + np.tanh(0.5 * x))
def train(self, rating_data, max_steps=1000, eps=1.0e-4):
'''迭代训练,得到连接权重
'''
for step in range(max_steps): # 迭代训练多少次
error = 0.0 # 误差平方和
# 每次拿一批样本还调整权重
for i in range(0, rating_data.shape[0], self.learn_batch):
num_examples = min(self.learn_batch, rating_data.shape[0] - i)
data = rating_data[i:i + num_examples, :]
data = np.insert(data, 0, 1, axis=1) # 第一列插入全1,即偏置的值初始化为1
pos_hidden_activations = np.dot(data, self.weights)
pos_hidden_probs = self._logistic(pos_hidden_activations)
pos_hidden_states = pos_hidden_probs > np.random.rand(
num_examples, self.num_hidden + 1)
# pos_associations=np.dot(data.T,pos_hidden_states) #对隐藏层作二值化
pos_associations = np.dot(
data.T, pos_hidden_probs) # 对隐藏层不作二值化
neg_visible_activations = np.dot(
pos_hidden_states, self.weights.T)
neg_visible_probs = self._logistic(neg_visible_activations)
neg_visible_probs[:, 0] = 1 # 强行把偏置的值重置为1
neg_hidden_activations = np.dot(
neg_visible_probs, self.weights)
neg_hidden_probs = self._logistic(neg_hidden_activations)
# neg_hidden_states=neg_hidden_probs>np.random.rand(num_examples,self.num_hidden+1)
# neg_associations=np.dot(neg_visible_probs.T,neg_hidden_states) #对隐藏层作二值化
neg_associations = np.dot(
neg_visible_probs.T, neg_hidden_probs) # 对隐藏层不作二值化
# 更新权重。另外一种尝试是带冲量的梯度下降,即本次前进的方向是本次梯度与上一次梯度的线性加权和(这样的话需要额外保存上一次的梯度)
self.weights += self.learn_rate * \
(pos_associations - neg_associations) / num_examples
# 计算误差平方和
error += np.sum((data - neg_visible_probs) ** 2)
if error < eps: # 所有样本的误差平方和低于阈值于终止迭代 break print('iteration %d, error is %f' % (step, error)) def getHidden(self, visible_data): '''根据输入层得到隐藏层 visible_data是一个matrix,每行代表一个样本 ''' num_examples = visible_data.shape[0] hidden_states = np.ones((num_examples, self.num_hidden + 1)) visible_data = np.insert(visible_data, 0, 1, axis=1) # 第一列插入偏置 hidden_activations = np.dot(visible_data, self.weights) hidden_probs = self._logistic(hidden_activations) hidden_states[:, :] = hidden_probs > np.random.rand(
num_examples, self.num_hidden + 1)
hidden_states = hidden_states[:, 1:] # 即首列删掉,即把偏置去掉
return hidden_states
def getVisible(self, hidden_data):
'''根据隐藏层得到输入层
hidden_data是一个matrix,每行代表一个样本
'''
num_examples = hidden_data.shape[0]
visible_states = np.ones((num_examples, self.num_visible + 1))
hidden_data = np.insert(hidden_data, 0, 1, axis=1)
visible_activations = np.dot(hidden_data, self.weights.T)
visible_probs = self._logistic(visible_activations)
visible_states[:, :] = visible_probs > np.random.rand(
num_examples, self.num_visible + 1)
visible_states = visible_states[:, 1:]
return visible_states
def predict(self, visible_data):
num_examples = visible_data.shape[0]
hidden_states = np.ones((num_examples, self.num_hidden + 1))
visible_data = np.insert(visible_data, 0, 1, axis=1) # 第一列插入偏置
'''forward'''
hidden_activations = np.dot(visible_data, self.weights)
hidden_probs = self._logistic(hidden_activations)
# hidden_states[:, :] = hidden_probs > np.random.rand(
# num_examples, self.num_hidden + 1)
'''backward'''
visible_states = np.ones((num_examples, self.num_visible + 1))
# visible_activations = np.dot(hidden_states, self.weights.T) #对隐藏层作二值化
visible_activations = np.dot(hidden_probs, self.weights.T) # 对隐藏层不作二值化
visible_probs = self._logistic(visible_activations) # 直接返回可视层的概率值
return visible_probs[:, 1:] # 把第0列(偏置)去掉
if __name__ == '__main__':
rbm = RBM(num_visible=6, num_hidden=2, learn_rate=0.1, learn_batch=1000)
rating_data = np.array([[1, 1, 1, 0, 0, 0], [1, 0, 1, 0, 0, 0], [1, 1, 1, 0, 0, 0], [
0, 0, 1, 1, 1, 0], [0, 0, 1, 1, 0, 0], [0, 0, 1, 1, 1, 0]])
rbm.train(rating_data, max_steps=500, eps=1.0e-4)
print('weight:\n', rbm.weights)
rating = np.array([[0, 0, 0, 0.9, 0.7, 0]]) # 评分需要做归一化。该用户喜欢第四、五项
hidden_data = rbm.getHidden(rating)
print('hidden_data:\n', hidden_data)
visible_data = rbm.getVisible(hidden_data)
print('visible_data:\n', visible_data)
predict_data = rbm.predict(rating)
print('推荐得分:')
for i, score in enumerate(predict_data[0, :]):
print(i, score) # 第三、四、五项的推荐得分很高,同时用户已明确表示过喜欢四、五,所以我们把第三项推荐给用户
参考资料
http://www.cnblogs.com/zhangchaoyang/articles/5537643.html
https://blog.csdn.net/doleria/article/details/78618022