天天看點

李航《統計學習方法》第2版 第10章 HMM實作分詞(代碼實作)

利用HMM模型實作分詞:

四種狀态:

B:詞語的開頭

M:一個詞語的中間詞

E:一個詞語的結果

S:非詞語,單個詞

學習:Baum-Welch算法

預測:維特比算法

資料集:人民日報1998年中文标注語料庫

連結:https://pan.baidu.com/s/1SKi9DUjxuh6tENfm6jmNCA

提取碼:hz3q

複制這段内容後打開百度網盤手機App,操作更友善哦

代碼引用自:www.pkudodo.com

#coding=utf-8
#Author:Dodo
#Date:2018-12-10
#Email:[email protected]
#Blog:www.pkudodo.com
'''
學習:Baum-Welch算法
預測:維特比算法
資料集:人民日報1998年中文标注語料庫
------------------------------
運作結果:
-------------------原文----------------------
深圳有個打工者閱覽室
去年12月,我在廣東深圳市出差,聽說南山區工商分局為打工者建了個免費圖書閱覽室,這件新鮮事引起了我的興趣。
12月18日下午,我來到了這個閱覽室。閱覽室位于桂廟,臨南油大道,是一間輕體房,面積約有40平方米,内部裝修得整潔幹淨,四周的書架上擺滿了書,并按政治、哲學、法律法規、文化教育、經濟、科技、藝術、中國文學、外國文學等分類,屋中央有兩排書架,上面也擺滿了圖書和雜志。一些打工青年或站或蹲,認真地閱讀,不時有人到借閱台前辦理借書或還書手續。南山區在深圳市西邊,地處城鄉結合部,外來打工者較多。去年2月,南山區工商分局局長王安全發現分局對面的公園裡常有不少打工者業餘時間閑逛,有時還滋擾生事。為了給這些打工者提供一個充實自己的場所,他提議由全分局從業人員捐款,興建一個免費閱覽室。上司帶頭,群衆響應,大家捐款1.4萬元,購買了近千冊圖書。3月6日,建在南頭繁華的南新路和金雞路交叉口的閱覽室開放了。從此,這裡每天都吸引了衆多借書、看書的人們,其中不僅有打工者,還有機關幹部、公司職員和個體戶。到了夏天,由于閱覽室所在地被工程征用,南山區工商分局便把閱覽室遷到了桂廟。閱覽室的管理人員是兩名青年,男的叫張攀,女的叫趙陽。張攀自己就是湖北來的打工者,聽說南山區工商分局辦免費閱覽室,便主動應聘來服務。閱覽室每天從早9時開到晚10時,夜裡張攀就住在這裡。他談起閱覽室裡的圖書,翻着一本本的借閱名冊,如數家珍,對圖書和工作的摯愛之情溢于言表。我在這裡碰到南山區華英大廈一位叫聶煜的女青年,她說她也是個打工者,由于春節探家回來後就要去市内工作,很留戀這裡的這個免費閱覽室,想抓緊時間多看些書,她還把自己買的幾本雜志捐給了閱覽室。在閱覽室的捐書登記簿上,記錄着這樣的數字:工商系統内部捐書3550冊,社會各界捐書250冊。我在閱覽室讀到了這樣幾封感謝信:深圳瑞興光學廠的王志明寫道:“我們這些年輕人遠離了家鄉,來到繁華緊張的都市打工,辛勞之餘,能有機會看書讀報,感到特别充實。”深圳文光燈泡廠的江虹說:“南山區工商分局的幹部職工捐款、捐書,給我們打工者提供良好的學習環境,鼓勵我們求知上進,真是辦了一件大好事,他們是我們打工者的知音。”(本報記者羅華)
-------------------分詞後----------------------
深圳|有個|打|工者|閱覽室
去年|12月|,|我|在|廣東|深圳|市出|差|,|聽|說|南|山區|工商|分局|為|打|工者|建了|個|免費|圖書|閱覽室|,|這件|新|鮮事|引起|了|我|的|興趣|。
12月|18日|下午|,|我來|到|了|這個|閱覽室|。|閱覽|室位|于|桂廟|,|臨南油|大道|,|是|一間|輕|體房|,|面積|約|有|40|平方|米|,|内|部裝|修得|整潔|幹淨|,|四|周|的|書架|上|擺滿|了|書|,|并|按|政治|、|哲學|、|法律|法規|、|文化|教育|、|經濟|、|科技|、|藝術|、|中國|文學|、|外國|文學|等|分類|,|屋|中央|有|兩排|書架|,|上面|也|擺滿|了|圖書|和|雜志|。|一些|打工|青年|或站|或|蹲|,|認真|地閱|讀|,|不時|有|人到|借閱|台前|辦理|借書|或|還書|手續|。|南|山區|在|深圳|市|西邊|,|地處|城鄉|結合部|,|外來|打|工者|較|多|。|去年|2月|,|南|山區|工商|分局|局長|王|安全|發現|分局|對面|的|公園|裡|常有|不少|打|工者|業餘|時間|閑逛|,|有時|還|滋擾|生事|。|為|了|給|這些|打|工者|提供|一個|充實|自己|的|場所|,|他|提議|由全|分局|工作|人員|捐款|,|興建|一個|免費|閱覽室|。|上司|帶頭|,|群衆|響應|,|大家|捐款|1.4萬|元|,|購買|了|近|千冊|圖書|。|3月|6日|,|建在|南頭|繁華|的|南|新路|和|金雞|路交|叉口|的|閱覽室|開放|了|。|從此|,|這裡|每天|都|吸引|了|衆多|借書|、|看書|的|人們|,|其中|不僅|有|打|工者|,|還|有|機關|幹部|、|公司|職員|和|個|體戶|。|到|了|夏天|,|由于|閱覽室|所|在|地|被|工程|征用|,|南|山區|工商|分局|便|把|閱覽|室遷|到|了|桂廟|。|閱覽室|的|管理|人員|是|兩|名|青年|,|男|的|叫|張|攀|,|女|的|叫|趙陽|。|張|攀|自己|就|是|湖北|來|的|打|工者|,|聽|說|南|山區|工商|分局|辦|免費|閱覽室|,|便|主動|應|聘來|服務|。|閱覽室|每天|從|早9時|開到|晚|10|時|,|夜裡|張|攀|就|住|在|這裡|。|他談|起|閱覽|室裡|的|圖書|,|翻着|一|本本|的|借閱|名冊|,|如數|家珍|,|對|圖書|和|工作|的|摯愛|之|情溢|于|言表|。|我|在|這裡|碰到|南|山區|華英|大廈|一位|叫|聶|煜|的|女|青年|,|她|說|她|也|是|個|打|工者|,|由于|春節|探家|回來|後|就|要|去市|内|工作|,|很|留戀|這裡|的|這個|免費|閱覽室|,|想|抓緊|時間|多|看些|書|,|她|還|把|自己|買|的|幾本|雜志|捐給|了|閱覽室|。|在|閱覽室|的|捐書|登|記簿|上|,|記錄|着|這樣|的|數字|:|工商|系統|内部|捐書|3550冊|,|社會|各界|捐書|250冊|。|我|在|閱覽室|讀到|了|這樣|幾|封感|謝信|:|深圳|瑞興|光學|廠|的|王|志明|寫道|:|“|我們|這些|年|輕人|遠離|了|家鄉|,|來到|繁華|緊張|的|都|市|打工|,|辛勞|之餘|,|能|有|機會|看書|讀報|,|感到|特别|充實|。|”|深圳|文光|燈|泡廠|的|江虹|說|:|“|南|山區|工商|分局|的|幹部|職工|捐款|、|捐書|,|給|我們|打|工者|提供|良好|的|學習|環境|,|鼓勵|我們|求知|上進|,|真是|辦|了|一件|大好|事|,|他們|是|我們|打|工者|的|知音|。|”|(|本報|記者|羅華|)

運作時長:3.6s
'''
import numpy as np
import time

def trainParameter(fileName):
    '''
    依據訓練文本統計PI、A、B
    :param fileName: 訓練文本
    :return: 三個參數
    '''
    #定義一個查詢字典,用于映射四種标記在數組中對應的位置,友善查詢
    # B:詞語的開頭
    # M:一個詞語的中間詞
    # E:一個詞語的結果
    # S:非詞語,單個詞
    statuDict = {'B':0, 'M':1, 'E':2, 'S':3}

    #每個字隻有四種狀态,是以下方的各類初始化中大小的參數均為4
    #初始化PI的一維數組,因為對應四種狀态,大小為4
    PI = np.zeros(4)
    #初始化狀态轉移矩陣A,涉及到四種狀态各自到四種狀态的轉移,因為大小為4x4
    A = np.zeros((4, 4))
    #初始化觀測機率矩陣,分别為四種狀态到每個字的發射機率
    #因為是中文分詞,使用ord(漢字)即可找到其對應編碼,這裡用一個65536的空間來保證對于所有的漢字都能
    #找到對應的位置來存儲
    B = np.zeros((4, 65536))
    #去讀訓練文本
    fr = open(fileName, encoding='utf-8')

    #文本中的每一行認為是一個訓練樣本
    #在統計上,三個參數依據“10.3.2” Baum-Welch算法内描述的統計
    #PI依據式10.35
    #A依據10.37
    #B依據10.38
    #注:并沒有使用Baum-Welch算法,隻是借助了其内部的三個參數生成公式,其實
    #公式并不是Baum-Welch特有的,隻是在那一節正好有描述
    for line in fr.readlines():
        #---------------------訓練集單行樣例--------------------
        #深圳  有  個  打工者  閱覽室
        #------------------------------------------------------
        #可以看到訓練樣本已經分詞完畢,詞語之間空格隔開,是以我們在生成統計時主要借助以下思路:
        # 1.先将句子按照空格隔開,例如例句中5個詞語,隔開後變成一個長度為5的清單,每個元素為一個詞語
        # 2.對每個詞語長度進行判斷:
        #       如果為1認為該詞語是S,即單個字
        #       如果為2則第一個是B,表開頭,第二個為E,表結束
        #       如果大于2,則第一個為B,最後一個為E,中間全部标為M,表中間詞
        # 3.統計PI:該句第一個字的詞性對應的PI中位置加1
        #           例如:PI = [0, 0, 0, 0],當本行第一個字是B,即表示開頭時,PI中B對應位置為0,
        #               則PI = [1, 0, 0, 0],全部統計結束後,按照計數值再除以總數得到機率
        #   統計A:對狀态鍊中位置t和t-1的狀态進行統計,在矩陣中相應位置加1,全部結束後生成機率
        #   統計B:對于每個字的狀态以及字内容,生成狀态到字的發射計數,全部結束後生成機率
        #   注:可以看一下“10.1.1 隐馬爾可夫模型的定義”一節中三個參數的定義,會有更清晰一點的認識
        #-------------------------------------------------------
        #對單行句子按空格進行切割
        curLine = line.strip().split()
        #對詞性的标記放在該清單中
        wordLabel = []
        #對每一個單詞進行周遊
        for i in range(len(curLine)):
            #如果長度為1,則直接将該字标記為S,即單個詞
            if len(curLine[i]) == 1:
                label = 'S'
            else:
                #如果長度不為1,開頭為B,最後為E,中間添加長度-2個M
                #如果長度剛好為2,長度-2=0也就不添加了,反之添加對應個數的M
                label = 'B' + 'M' * (len(curLine[i]) - 2) + 'E'

            #如果是單行開頭第一個字,PI中對應位置加1,
            if i == 0: PI[statuDict[label[0]]] += 1

            #對于該單詞中的每一個字,在生成的狀态鍊中統計B
            for j in range(len(label)):
                #周遊狀态鍊中每一個狀态,并找到對應的中文漢字,在B中
                #對應位置加1
                B[statuDict[label[j]]][ord(curLine[i][j])] += 1

            #在整行的狀态鍊中添加該單詞的狀态鍊
            #注意:extend表直接在原先元素的後方添加,
            #可以百度一下extend和append的差別
            wordLabel.extend(label)

        #單行所有單詞都結束後,統計A資訊
        #因為A涉及到前一個狀态,是以需要等整條狀态鍊都生成了才能開始統計
        for i in range(1, len(wordLabel)):
            #統計t時刻狀态和t-1時刻狀态的所有狀态組合的出現次數
            A[statuDict[wordLabel[i - 1]]][statuDict[wordLabel[i]]] += 1

    #上面代碼在統計上全部是統計的次數,實際運算需要使用機率,
    #下方代碼是将三個參數的次數轉換為機率
    #----------------------------------------
    #對PI求和,機率生成中的分母
    sum = np.sum(PI)
    #周遊PI中每一個元素,元素出現的次數/總次數即為機率
    for i in range(len(PI)):
        #如果某元素沒有出現過,該位置為0,在後續的計算中這是不被允許的
        #比如說某個漢字在訓練集中沒有出現過,那在後續不同機率相乘中隻要有
        #一項為0,其他都是0了,此外整條鍊很長的情況下,太多0-1的機率相乘
        #不管怎樣最後的結果都會很小,很容易下溢出
        #是以在機率上我們習慣将其轉換為log對數形式,這在書上是沒有講的
        #x大的時候,log也大,x小的時候,log也相應小,我們最後比較的是不同
        #機率的大小,是以使用log沒有問題

        #那麼當單向機率為0的時候,log沒有定義,是以需要單獨判斷
        #如果該項為0,則手動賦予一個極小值
        if PI[i] == 0:  PI[i] = -3.14e+100
        #如果不為0,則計算機率,再對機率求log
        else: PI[i] = np.log(PI[i] / sum)

    #與上方PI思路一樣,求得A的機率對數
    for i in range(len(A)):
        sum = np.sum(A[i])
        for j in range(len(A[i])):
            if A[i][j] == 0: A[i][j] = -3.14e+100
            else: A[i][j] = np.log(A[i][j] / sum)

    #與上方PI思路一樣,求得B的機率對數
    for i in range(len(B)):
        sum = np.sum(B[i])
        for j in range(len(B[i])):
            if B[i][j] == 0: B[i][j] = -3.14e+100
            else:B[i][j] = np.log(B[i][j] / sum)

    #傳回統計得到的三個參數
    return PI, A, B

def loadArticle(fileName):
    '''
    加載文章
    :param fileName:檔案路徑
    :return: 文章内容
    '''
    #初始化文章清單
    artical = []
    #打開檔案
    fr = open(fileName, encoding='utf-8')
    #按行讀取檔案
    for line in fr.readlines():
        #讀到的每行最後都有一個\n,使用strip将最後的回車符去掉
        line = line.strip()
        #将該行放入文章清單中
        artical.append(line)
    #将文章傳回
    return artical

def participle(artical, PI, A, B):
    '''
    分詞
    算法依據“10.4.2 維特比算法”
    :param artical:要分詞的文章
    :param PI: 初始狀态機率向量PI
    :param A: 狀态轉移矩陣
    :param B: 觀測機率矩陣
    :return: 分詞後的文章
    '''
    #初始化分詞後的文章清單
    retArtical = []

    #對文章按行讀取
    for line in artical:
        #初始化δ,δ存放四種狀态的機率值,因為狀态鍊中每個狀态都有
        #四種機率值,是以長度時該行的長度
        delta = [[0 for i in range(4)] for i in range(len(line))]
        #依據算法10.5 第一步:初始化
        for i in range(4):
            #初始化δ狀态鍊中第一個狀态的四種狀态機率
            delta[0][i] = PI[i] + B[i][ord(line[0])]
        #初始化ψ,初始時為0
        psi = [[0 for i in range(4)] for i in range(len(line))]

        #算法10.5中的第二步:遞推
        #for循環的符号與書中公式一緻,可以對比着看來了解
        #依次處理整條鍊
        for t in range(1, len(line)):
            #對于鍊中的米格狀态,求四種狀态機率
            for i in range(4):
                #初始化一個臨時清單,用于存放四種機率
                tmpDelta = [0] * 4
                for j in range(4):
                    # 計算第二步中的δ,該部分隻計算max内部,不涉及後面的bi(o)
                    # 計算得到四個結果以後,再去求那個max即可
                    # 注:bi(Ot)并不在max的式子中,是求出max以後再乘b的
                    #   此外讀者可能注意到書中的乘法在這裡變成了加法,這是由于原先是機率
                    #   直接相乘,但我們在求得機率時,同時取了log,取完log以後,機率的乘法
                    #   也就轉換為加法了,同時也簡化了運算
                    #   是以log優點還是很多的對不?
                    tmpDelta[j] = delta[t - 1][j] + A[j][i]

                #找到最大的那個δ * a,
                maxDelta = max(tmpDelta)
                #記錄最大值對應的狀态
                maxDeltaIndex = tmpDelta.index(maxDelta)

                #将找到的最大值乘以b放入,
                #注意:這裡同樣因為log變成了加法
                delta[t][i] = maxDelta + B[i][ord(line[t])]
                #在ψ中記錄對應的最大狀态索引
                psi[t][i] = maxDeltaIndex

        #建立一個狀态鍊清單,開始生成狀态鍊
        sequence = []
        #算法10.5 第三步:終止
        #在上面for循環全部結束後,很明顯就到了第三步了
        #擷取最後一個狀态的最大狀态機率對應的索引
        i_opt = delta[len(line) - 1].index(max(delta[len(line) - 1]))
        #在狀态鍊中添加索引
        #注:狀态鍊應該是B、M、E、S,這裡圖友善用了0、1、2、3,其實一樣的
        sequence.append(i_opt)
        #算法10.5 第四步:最優路徑回溯
        #從後往前周遊整條鍊
        for t in range(len(line) - 1, 0, -1):
            #不斷地從目前時刻t的ψ清單中讀取到t-1的最優狀态
            i_opt = psi[t][i_opt]
            #将狀态放入清單中
            sequence.append(i_opt)
        #因為是從後往前将狀态放入的清單,是以這裡需要翻轉一下,變成了從前往後
        sequence.reverse()

        #開始對該行分詞
        curLine = ''
        #周遊該行每一個字
        for i in range(len(line)):
            #在清單中放入該字
            curLine += line[i]
            #如果該字是3:S->單個詞  或  2:E->結尾詞 ,則在該字後面加上分隔符 |
            #此外如果改行的最後一個字了,也就不需要加 |
            if (sequence[i] == 3 or sequence[i] == 2) and i != (len(line) - 1):
                curLine += '|'
        #在傳回清單中添加分詞後的該行
        retArtical.append(curLine)
    #傳回分詞後的文章
    return retArtical

if __name__ == '__main__':
    # 開始時間
    start = time.time()

    #依據現有訓練集統計PI、A、B
    PI, A, B = trainParameter('HMMTrainSet.txt')

    #讀取測試文章
    artical = loadArticle('testArtical.txt')

    #列印原文
    print('-------------------原文----------------------')
    for line in artical:
        print(line)

    #進行分詞
    partiArtical = participle(artical, PI, A, B)

    #列印分詞結果
    print('-------------------分詞後----------------------')
    for line in partiArtical:
        print(line)

    #結束時間
    print('time span:', time.time() - start)

           

繼續閱讀