“聊天的秘密”之HMM读心术

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/firparks/article/details/54631541

世界上最遥远的距离,不是生与死,而是我不知道你在说啥。。

老王有次跟我吐槽,说是“现在越来越不明白女人心里都想的啥”,为了证实他自己的观点,他给我看了一张聊天的截图:

这里写图片描述

我凑过去看了看,看完也是云里雾里。于是为了帮助老王,我拍着胸口说:“没问题,让我来给你破解跟女人聊天的秘密吧!”老王半信半疑的答应了。


没过多久,我从截图里找到了女人说话的两个规律:

1)当时的心情决定了当时说的话 (感性)
2)心情只跟前一秒的心情有关系 (善变)

我一拍大腿,这不就是隐马尔科夫模型(HMM)的基本假设嘛。

既然选择了HMM,那么我们不妨来看看是怎么去定义HMM的:
这里写图片描述

这说的是什么意思?看图:
这里写图片描述

需要说明的是,隐马尔科夫模型之所以叫“隐”,而不是马尔科夫模型,是因为状态集合{S1,S2,S3…Sn}是看不见的,就跟隐身了一样。一言以蔽之,就是“状态S通过符号发射概率b产生输出符号K,状态S通过转移的概率a转移到下一个状态从而产生输出符号K的序列”


在我们的“聊天的秘密”问题中,女人的心情大致分为{高兴,不高兴}两种,于是我将状态集合S=(“高兴”,”不高兴”)两种,这同时说明了状态S1有可能是高兴也有可能是不高兴,S2…Sn也是如此,这些不同的状态最后都会转化为女人所说的话。这与我们的第一个假设“当时的心情决定了当时说的话”不谋而合。

然后通过最开始的截图,我们发现在这次聊天里,该女人一共说了三种话,即输出符号K=(“你好坏呀”,”你有毛病吧”,”你猜”)

然后我们可以定义一个状态转移概率A,如下图所示:

这里写图片描述

这张图其实说的是我们的第二个假设“心情只跟前一秒的心情有关系”,假设女人在高兴的时候,下一秒还会高兴的概率是0.4,下一秒是不高兴了的概率是0.6,而在不高兴时,下一秒继续不高兴的概率是0.8,下一秒变高兴的概率是0.2。形式化的定义为:A={ “高兴”:{“高兴”:0.4,”不高兴”:0.6}, “不高兴”:{“高兴”:0.2,”不高兴”:0.8} }

接着我们定义符号发射概率B
举例来说,假设女人在高兴的时候,会说”你好坏呀”的概率是0.6,会说“你有毛病吧”的概率是0.3,会说“你猜”的概率是0.1。
这里写图片描述
形式化的定义为:
B={
“高兴”:{“你好坏呀”:0.6,”你有毛病吧”:0.3,”你猜”:0.1},
“不高兴”:{“你好坏呀”:0.1,”你有毛病吧”:0.7,”你猜”:0.2}
}

最后,我们只缺初始状态的概率分布π的定义了,Pi={“高兴”:0.7,”不高兴”:0.3}

定义完以后,我们观察到截图中的话依次是”你有毛病吧”,”你好坏呀”,”你猜”,”你有毛病吧”,”你猜”,即观察序列O=O=[K[1],K[0],K[2],K[1],K[2]],K是我们之前定义过的输出符号。

程序的定义如下:

 #S为状态的集合
    S=("高兴","不高兴")
    #K为输出符号的集合
    K=("你好坏呀","你有毛病吧","你猜")
    #pi为初始状态的概率分布
    Pi={"高兴":0.7,"不高兴":0.3}
    #A为状态转移概率
    A={
       "高兴":{"高兴":0.4,"不高兴":0.6},
       "不高兴":{"高兴":0.2,"不高兴":0.8}
       }
    #B为符号发射概率
    B={
       "高兴":{"你好坏呀":0.6,"你有毛病吧":0.3,"你猜":0.1},
       "不高兴":{"你好坏呀":0.1,"你有毛病吧":0.7,"你猜":0.2}
       }
    #O为观察序列
    O=[K[1],K[0],K[2],K[1],K[2]]

在HMM中有三个基本问题,即:
这里写图片描述

我们的案例中,只需要解决第二个问题,即我想看一下女人的心路历程,也就是S的变化。
多说一句,第一个问题可以用前向/后项算法解决,第二个问题可以用维特比算法解决,第三个问题可以用参数估计解决。
而由于维特比算法是由前向算法演变而来的,所以我们先来看看前向算法是怎么一回事。

前向算法是这么定义的:
这里写图片描述
这里写图片描述

其实理解起来也挺简单,我们可以用是格架的形式来说明,如图
这里写图片描述
从直觉上看,就是前面所有的状态Si决定了下一个状态Sj
用马克思的话说就是:

“量变引起质变”

我们用程序模拟这一过程:

    #前向算法:1.初始化,2:归纳计算,3:求和终结
    def forwardProcedure(self):
        #初始化
        sums=0.0
        num=0
        p=0
        count=1
        #申请相应的二元数组
        alpha=[[0 for col in range(0,len(S))] for row in range(0,len(O))]
        for keys in Pi.keys():
            alpha[0][num]=Pi[keys]*B[keys][O[0]]
            num+=1

        #归纳计算
        for times in range(len(O)-1):
            #当前的行
            rowNum=int(num/len(S))
            #当前的列
            colNum=num%len(S)
            for j in S:
                for i in A.keys():
                    #累加的前半部分
                    sums+=alpha[rowNum-1][colNum]*A[i][j]
                #根据前面所有节点计算出下一个节点,并存入二维数组
                alpha[rowNum][colNum]=sums*B[j][O[count]]
                colNum+=1
                num+=1
            count+=1

        #求和终结
        for final in range(len(S)):
            p+=alpha[len(O)-1][final]
        return p

接下来我们就可以用类似的思路来解决HMM第二类问题了,即维特比算法。
维特比算法较之前向算法的高明之处是,维特比算法不用来求所有的Si值,而是用一个序列来记录其中最大的Si,最后回溯路径。
这也是动态规划的思路,找到其中的最大权值的路径。

维特比变量的定义如下:
这里写图片描述
然后维特比算法的过程是这样的:
这里写图片描述
注意比较两个算法的形式,你会发现,维特比算法和前向算法的时间复杂度是一致的,都是O(N2T)
同样,我们也用格架图来描述:
这里写图片描述

最后用程序模拟:

    #维特比算法:1.初始化,2.归纳计算,3.终结,4.路径回溯
    def viterbi(self):

        #申请维特比变量delta和记录路径的变量path
        delta=[0]*len(S)
        paths=[]
        num=0
        count=1

        #初始化
        for i in Pi.keys():
            delta[num]=Pi[i]*B[i][O[0]]
            num=(num+1)%len(S)
        paths.append(S[delta.index(max(delta))])

        #归纳计算
        for i in range(len(O)-1):
            maxium=max(delta)
            position=delta.index(maxium) 
            for j in S:
                delta[num]=maxium*A[S[position]][j]*B[j][O[count]]
                num=(num+1)%len(S)
            count+=1
            #记录路径
            paths.append(S[position])
        return paths

至此,我们已经完成了几乎所有的工作,让程序跑起来吧!(完整程序见附录)

这里写图片描述

老王的问题终于解决了!
老王看到结果后,感叹道:女人心,海底针呐。☺


附录:
“聊天的秘密”之HMM读心术

'''
Created on 2017年1月20日

@author: 薛沛雷
'''


class HMM:

    #HMM五元组(S,K,A,B,Pi)初始化
    def __init__(self,S,K,A,B,Pi,O):
        self.S=S
        self.K=K
        self.A=A
        self.B=B
        self.Pi=Pi
        self.O=O

    #前向算法:1.初始化,2:归纳计算,3:求和终结
    def forwardProcedure(self):
        #初始化
        sums=0.0
        num=0
        p=0
        count=1
        #申请相应的二元数组
        alpha=[[0 for col in range(0,len(S))] for row in range(0,len(O))]
        for keys in Pi.keys():
            alpha[0][num]=Pi[keys]*B[keys][O[0]]
            num+=1

        #归纳计算
        for times in range(len(O)-1):
            #当前的行
            rowNum=int(num/len(S))
            #当前的列
            colNum=num%len(S)
            for j in S:
                for i in A.keys():
                    #累加的前半部分
                    sums+=alpha[rowNum-1][colNum]*A[i][j]
                #根据前面所有节点计算出下一个节点,并存入二维数组
                alpha[rowNum][colNum]=sums*B[j][O[count]]
                colNum+=1
                num+=1
            count+=1

        #求和终结
        for final in range(len(S)):
            p+=alpha[len(O)-1][final]
        return p





    #维特比算法:1.初始化,2.归纳计算,3.终结,4.路径回溯
    def viterbi(self):

        #申请维特比变量delta和记录路径的变量path
        delta=[0]*len(S)
        paths=[]
        num=0
        count=1

        #初始化
        for i in Pi.keys():
            delta[num]=Pi[i]*B[i][O[0]]
            num=(num+1)%len(S)
        paths.append(S[delta.index(max(delta))])

        #归纳计算
        for i in range(len(O)-1):
            maxium=max(delta)
            position=delta.index(maxium) 
            for j in S:
                delta[num]=maxium*A[S[position]][j]*B[j][O[count]]
                num=(num+1)%len(S)
            count+=1
            #记录路径
            paths.append(S[position])
        return paths




if __name__ == "__main__":

    #S为状态的集合
    S=("高兴","不高兴")
    #K为输出符号的集合
    K=("你好坏呀","你有毛病吧","你猜")
    #pi为初始状态的概率分布
    Pi={"高兴":0.7,"不高兴":0.3}
    #A为状态转移概率
    A={
       "高兴":{"高兴":0.4,"不高兴":0.6},
       "不高兴":{"高兴":0.2,"不高兴":0.8}
       }
    #B为符号发射概率
    B={
       "高兴":{"你好坏呀":0.6,"你有毛病吧":0.3,"你猜":0.1},
       "不高兴":{"你好坏呀":0.1,"你有毛病吧":0.7,"你猜":0.2}
       }
    #O为观察序列
    O=[K[1],K[0],K[2],K[1],K[2]]

    hmm=HMM(S,K,A,B,Pi,O)
    print("前向算法计算出的概率为:%s"%hmm.forwardProcedure())
    print("viterbi算法计算出女人的心路历程是:%s"%hmm.viterbi())


有人问我:“为什么要实现这些基础的算法?以后能用得着吗?”
我想起朱熹的一句话,大概这就是答案吧。

《朱子家训》:宜未雨而绸缪,毋临渴而掘井。

完!

展开阅读全文

读心术,求高手指教!

06-01

rn读心术rnrn这个程序有着能够看穿你心思的特殊能力,试试吧!rnrnrn请你在心里默想一个两位数的数字(比方说54)rnrn用你刚才想到的数字分别减去其十位数和个位数得到结果(比方说54 - 5 - 4 = 结果45)rnrn在以下这个表格中查到你得出的结果,看看右边的符号是什么rnrn请集中精力在你查到的符号上面(不要把鼠标指到上面)至少5秒钟,然后点击下面这个魔力方块,看看预测的结果吧rnrnrnrnrn rn99 n 98 a 97 b 96 O 95 ^ 94 i 93 J 92 d 91 d 90 O rn89 R 88 m 87 I 86 x 85 6 84 6 83 f 82 81 T 80 z rn79 T 78 d 77 f 76 J 75 R 74 T 73 z 72 T 71 U 70 i rn69 u 68 f 67 S 66 J 65 I 64 x 63 T 62 h 61 J 60 d rn59 J 58 6 57 z 56 x 55 d 54 T 53 6 52 N 51 J 50 M rn49 m 48 l 47 ^ 46 O 45 T 44 z 43 b 42 6 41 b 40 6 rn39 U 38 z 37 R 36 T 35 i 34 T 33 d 32 T 31 n 30 f rn29 h 28 U 27 T 26 z 25 R 24 I 23 S 22 O 21 M 20 T rn19 m 18 T 17 O 16 R 15 l 14 x 13 S 12 u 11 R 10 R rn9 T 8 n 7 u 6 T 5 R 4 6 3 u 2 z 1 v 0 T rnrn rnrnrn如果觉得准就发给你的朋友 rnrnrnrnrnrn这是我的程序:rnrnrnrn#include rn#include rn#include rn rn int main()rnrn printf("请你在心里默想一个两位数的数字(比方说54)\n");rn printf("用你刚才想到的数字分别减去其十位数和个位数得到结果(比方说54 - 5 - 4 = 结果45)\n");rn printf("在以下这个表格中查到你得出的结果,看看右边的符号是什么\n");rn printf("请集中精力在你查到的符号上面(不要把鼠标指到上面)至少5秒钟,然后按任意键,看看预测的结果吧\n");rnrn int n = 0, i, j;rn int inumber[10][10];rn char csign[10][10];rn rn srand( (unsigned)time( NULL ) );rnrn for(i = 0; i < 10; i++)rn rn for(j = 0; j < 10; j++, n++)rn rn inumber[i][j] = n;rn csign[i][j] = rand()%(243 - 144 + 1) + 144;rn if(n % 9 == 0) csign[i][j] = csign[0][9];rn printf("%-3d%-3c", inumber[i][j], csign[i][j]); rn rn printf("\n");rn rnrn system("pause");rnrn printf("%c\n", csign[0][9]);rnrnrn return 0 ;rnrn 论坛

没有更多推荐了,返回首页