python實現貝葉斯網絡的概率推導(Probabilistic Inference)


寫在前面

這是HIT2019人工智能實驗三,由於時間緊張,代碼沒有進行任何優化,實驗算法僅供參考。

實驗要求

實現貝葉斯網絡的概率推導(Probabilistic Inference)

具體實驗指導書見github

這里首先給出代碼

知識部分

關於貝葉斯網絡的學習,我參考的是這篇博客

貝葉斯網絡(belief network)

這篇博客講述的雖然全面,但細節部分,尤其是貝葉斯網絡概率推導的具體實現部分,一筆帶過。然而本次實驗的要求就是實現貝葉斯網絡的概率推導,因此我在學習完這篇博客的基礎上,又把老師發的ppt學了一遍,(由於ppt是英文的,一開始我是拒絕學的),最后又挑重點看了下博客和ppt,感覺豁然開朗。

因此如果沒有學習過貝葉斯網絡,建議按照我上面列出的順序學習。

由於ppt較大,因此這里以網盤形式給出,提取碼:cn3h,該ppt僅供個人學習參考,嚴禁以盈利形式傳播

關於貝葉斯網絡的概率推導,最重要的公式是以下這兩個:

圖一

圖二

這兩個公式具體什么意思,網上或者是ppt中都有講解,這里不再贅述。重點在於這兩個公式是完成本實驗代碼的核心公式,這一點我在完成實驗之后才意識到,在之前學習ppt的時候,由於公式眾多,並沒有意識到這兩個公式的重要性。

實驗代碼

代碼所在的github地址已給出

需要注意的是,由於該實驗指定了數據格式,因此代碼完全是在指定數據格式要求下完成的,不具有普適性,因此實驗代碼僅供參考算法使用

設計的cpt格式如下:

class cpt:
    def __init__(self, name, parents, probabilities):
        self.name = name
        self.parents = parents
        self.probabilities = probabilities

貝葉斯網絡代碼如下

from cpt import cpt

class BN:
    def __init__(self, nums, variables, graph, cpts):
        self.nums = nums
        self.variables = variables
        self.graph = graph
        self.cpts = cpts
        # 創建一個名字與編號的字典,便於查找
        index_list = [i for i in range(self.nums)]
        self.variables_dict = dict(zip(self.variables, index_list))
        # 計算全概率矩陣
        self.TotalProbability = self.calculateTotalProbability()

    def calculateProbability(self, event):
        # 分別計算待求變量個數k1和待消除變量個數k2,剩余的為條件變量個數
        k1 = self.count(event, 2)
        k2 = self.count(event, 3)

        probability = []
        for i in range(2**k1):
            p = 0
            for j in range(2**k2):
                index = self.calculateIndex(self.int2bin_list(i, k1), self.int2bin_list(j, k2), event)
                p = p + self.TotalProbability[index]
            probability.append(p)
        # 最后輸出的概率矩陣的格式:先輸出true,再輸出false
        return list(reversed([x/sum(probability) for x in probability]))

    def calculateTotalProbability(self):
        # 全概率矩陣為一個1 * 2^n大小的矩陣,將列號轉化為2進制,可表示事件的發生情況
        # 例如共有5個變量,則第7列的概率為p,表示事件00111(12不發生,345發生)發生的概率為p
        TotalProbability = [0 for i in range(2 ** self.nums)]
        for i in range(2 ** self.nums):
            p = 1
            binary_list = self.int2bin_list(i,self.nums)
            for j in range(self.nums):
                # 分沒有父節點和有父節點的情況
                # 注意python float在相乘時會產生不精確的問題,因此每次相乘前先乘1000將其轉化成整數相乘,最后再除回來
                if self.cpts[j].parents == []:
                    p = p * (self.cpts[j].probabilities[0][1-binary_list[j]] * 1000)
                else:
                    parents_list = self.cpts[j].parents
                    parents_index_list = [self.variables_dict[k] for k in parents_list]
                    index = self.bin_list2int([binary_list[k] for k in parents_index_list])
                    p = p * (self.cpts[j].probabilities[index][1 - binary_list[j]] * 1000)
            TotalProbability[i] = p / 10 ** (self.nums * 3)
        return TotalProbability

    def int2bin_list(self, a, b):
        # 將列號轉化成指定長度的二進制數組
        # 下面兩句話的含義:將a轉化成二進制字符串,然后分割成字符串數組,再將字符串數組轉化成整形數組
        # 若得到的整型數組長度不滿足self.nums,則在前面補上相應的零
        binary_list = list(map(int, list(bin(a).replace("0b", ''))))
        binary_list = (b - len(binary_list)) * [0] + binary_list
        return binary_list

    def bin_list2int(self, b):
        # 將二進制的數組轉化成整數
        result = 0
        for i in range(len(b)):
            result = result + b[len(b)-1-i] * (2 ** i)
        return result

    def calculateIndex(self, i, j, event):
        # 用於生成下標
        # 原理暫略
        index_list = []
        for k in range(len(event)):
            if event[k] == 2:
                index_list.append(i[0])
                del(i[0])
            elif event[k] == 3:
                index_list.append(j[0])
                del(j[0])
            else:
                index_list.append(event[k])

        return self.bin_list2int(index_list)

    def count(self, list, a):
        # 用於統計一個list中含有多少個指定的數字
        c = 0
        for i in list:
            if i == a:
                c = c + 1
        return c

該實驗的主程序(包括讀取指定數據文件的函數)如下:

import sys

from BN import BN
from cpt import cpt

# 讀取文件並生成一個貝葉斯網絡
def readBN(filename):
    f = open(filename, 'r')
    # 讀取變量數
    nums = int(f.readline())
    f.readline()
    # 讀取變量名稱
    variables = f.readline()[:-1].split(' ')
    f.readline()
    # 讀取有向圖鄰接表
    graph = []
    for i in range(nums):
        line = f.readline()[:-1].split(' ')
        graph.append(list(map(int, line)))
    f.readline()
    # 讀取cpt表
    # 注意,文件中數據格式必須完全按照指定要求,不可有多余的空行或空格
    cpts = []
    for i in range(nums):
        probabilities = []
        while True:
            line = f.readline()[:-1].split(' ')
            if line != ['']:
                probabilities.append(list(map(float, line)))
            else:
                break
        CPT = cpt(variables[i], [], probabilities)
        cpts.append(CPT)
    f.close()
    # 根據鄰接表為每個節點生成其父親節點
    # 注意,這里父親節點的順序是按照輸入的variables的順序排列的,不保證更換測試文件時的正確性
    for i in range(nums):
        for j in range(nums):
            if graph[i][j] == 1:
                cpts[j].parents.append(variables[i])

    # 測試父節點生成情況
    # for i in range(nums):
    #     print(cpts[i].parents)
    bayesnet = BN(nums, variables, graph, cpts)
    return bayesnet

# 讀取需要求取概率的命令
def readEvents(filename, variables):
    # 條件概率在本程序中的表示:
    # 對變量分類,2表示待求的變量,3表示隱含的需要被消去的變量,0和1表示條件變量的false和true
    # 例如變量為[Burglar, Earthquake, Alarm, John, Mary]
    # 待求的條件概率為P(Burglar | John=true, Mary=false),則event為[2, 3, 3, 1, 0]
    f = open(filename, 'r')
    events = []
    while True:
        line = f.readline()
        event = []
        if line == "\n":
            continue
        elif not line:
            break
        else:
            for v in variables:
                index = line.find(v)
                if index != -1:
                    if line[index+len(v)] == ' ' or line[index+len(v)] == ',':
                        event.append(2)
                    elif line[index+len(v)] == '=':
                        if line[index+len(v)+1] == 't':
                            event.append(1)
                        else:
                            event.append(0)
                else:
                    event.append(3)
            # 檢查文本錯誤
            if len(event) != len(variables):
                sys.exit()
            events.append(event)
    return events

# 主程序
filename1 = "burglarnetwork.txt"
bayesnet = readBN(filename1)
filename2 = "burglarqueries.txt"
events = readEvents(filename2, bayesnet.variables)
for event in events:
    print(bayesnet.calculateProbability(event))

知識總結

這一部分主要記錄在實驗過程中參考的博客,方便之后復習

由於沒有系統學過python,其中有挺多都是python基本技巧的,看來以后還要系統學一遍

python中判斷readline讀到文件末尾
這篇博客參考的是讀文件時如何判斷讀完

python 字符串和整數,浮點型互相轉換
這篇博客參考的是如何將從文件讀進來的文本轉化成數據

python-使用列表創建字典
這篇博客參考的是用list創建字典的方式

python在字符串中查找字符
在Python中,如何將一個字符串數組轉換成整型數組
Python-8、Python如何將整數轉化成二進制字符串
這三篇博客同樣是在處理讀入數據時參考的

Python3浮點型(float)運算結果不正確處理辦法
由於多個浮點數的概率在連乘的時候,導致出現了較大誤差,因此查了這篇博客,不過最后沒有使用Decimal模塊,而是直接乘1000再除1000解決了。

Python 技巧(三)—— list 刪除一個元素的三種做法

python numpy查詢數組是否有某個數的總個數
這篇博客,我試了一下發現不可以,報錯說不可以對布爾類型求和,恐怕是python版本的問題吧,這個我暫時沒有深究,自己寫了一個count函數

python list中數字與一個數相乘
對於list中一個數字與一個數相乘的方法,網上普遍給出的另一種方法是用numpy庫,其生成的數組可以直接與數相乘。然而由於我全程沒有用到numpy,不想在這個地方單獨用個numpy,所以采用了本篇博客中的方法。

python反轉列表的三種方式
由於實驗指導書指定的輸出結果與我算出來的相反,因此翻轉了一次列表

實驗總結

用一句話總結該實驗的作用:使我對於貝葉斯網絡的概率推導過程理解的更加透徹

做完實驗才意識到如果沒有手推幾個貝葉斯網絡的概率推導,那幾乎相當於沒有學,要是放到考試絕對寫不出來(想起了之前聽覺考試,平時沒有練習過手推隱馬爾科夫,導致考試的時候給了一個很簡單的HMM,最后由於太不熟練導致時間不足而沒有寫完)

整個實驗過程比較順暢,總時間大致8小時左右,其中寫代碼時間很短,全程幾乎沒有遇到bug,花時間的地方在於如何設計表示條件概率。這個東西花了我特別長的時間,最后的形式個人感覺不是特別簡潔,但是放在程序里還是挺好用的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM