rbm受限波兹曼算法理解

3,815次阅读
没有评论

共计 7646 个字符,预计需要花费 20 分钟才能阅读完成。

本文主要是参考了http://blog.echen.me/2011/07/18/introduction-to-restricted-boltzmann-machines/ 这篇文章,这个文章从二分因子的角度去理解,真的是讲得清新脱俗。

背景:假设你要求一群用户从0-100分来给一组电影打分。在经典的因子分析中,你可以尝试依据一组隐藏因子来解释每部电影及用户。例如,像星球大战和指环王这类电影与“科幻小说和魔幻”这类隐藏因子可能强相关,而喜欢瓦力和玩具总动员的用户与“皮克斯动画工作室”这一隐藏因子可能强相关。

从到这的分析可以大致看出,我们需要是发现背后的因子与当前我们实际观察的数据之间的关系。

RBM可以理解为一种二值化的因子分析法(这是对它的一种理解方式,当然还有其他的理解方式及用途,原文作者主要采用二值化因子分析法去解释并实现)。与以往让用户采用一个连续的分数段为电影打分不同的是,这里只是告诉你他们“喜欢”还是“不喜欢”一部电影(1或0),之后RBM将会尝试找到之所以这样去选择电影的隐藏因子。

例如,假设我们有一组共六部电影(哈利·波特、阿凡达、指环王3、角斗士、泰坦尼克号和星梦泪痕)并且让用户告诉我们他们想看哪些。如果我们想学到两个隐藏单元潜在的电影偏好—比如,在我们六部电影里呈现出了两个自然分组:“科幻小说/魔幻”组(包括哈利·波特、阿凡达和指环王3)以及“奥斯卡获得者”组(包括指环王、角斗士和泰坦尼克号),所以我们可能希望我们的隐藏单元将会与这些分组相对应—那么,RBM可能看起来像这样:

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受限波兹曼算法理解

请注意,第一个隐藏单元看起来对应“奥斯卡获得者”,第二个隐藏单元看起来对应“科幻小说/魔幻”,跟我们预期一致。
如果给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

正文完
请博主喝杯咖啡吧!
post-qrcode
 
admin
版权声明:本站原创文章,由 admin 2019-03-23发表,共计7646字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
评论(没有评论)
验证码