隱馬爾可夫分詞

前言

雖然目前 nlp 不少任務已經發展到了使用深度學習的循環神經網絡模型和注意力模型,但傳統的模型我們也同樣要了解。這裏看下如何使用隱馬爾科夫模型(HMM)進行分詞。mysql

隱馬爾科夫模型

隱馬爾科夫模型是一種有向圖模型,圖模型能清晰表達變量相關關係的機率,常見的圖模型還有條件隨機場,節點表示變量,節點之間的連線表示二者相關機率。用如下定義來描述HMM模型,git

設系統全部可能的狀態集合爲S = \{s_1,s_2,...s_n\},全部能觀察的對象的集合V=\{v_1,v_2,...v_m\},那麼就一共有n種隱狀態和m種顯狀態。github

再設總共T個時刻的狀態序列E = (e_1,e_2,...e_T),對應有T個時刻的觀察序列O = (o_1,o_2,...o_T),這兩個很容易理解,對應到nlp的詞性標註中就是一句話和這句話的詞性標註,好比「我/是/中國/人」和「代詞/動詞/名詞/名詞」。算法

隱馬爾科夫模型主要有三組機率:轉移機率、觀測機率和初始狀態機率。sql

系統狀態之間存在着轉移機率,設一個轉移矩陣A = [a_{ij}]_{n \times n},由於有n中隱狀態,因此是n行n列。機率的計算公式爲,bash

a_{ij} = P(e_{t+1} = s_j | e_t =s_i) ,\quad  1 \le i,j \le n

此表示任意時刻t的狀態若爲s_i,則下一時刻狀態爲s_j的機率,即任意時刻兩種狀態的轉移機率了。網絡

觀測機率矩陣爲B = [b_{ij}]_{n \times m},其中b_{ij} = P(o_t=v_j|e_t=s_i), \quad 1 \le i\le n,1 \le j\le m,它用於表示在任意時刻t,若狀態爲s_i,則生成觀察狀態v_k的機率。併發

除此以外還有一個初始狀態機率爲\pi =(\pi_1,\pi_2,...,\pi_n),用於表示初始時刻各狀態出現的機率,其中\pi_i=P(e_1=s_i),\quad i=1,2,...,n,即t=1時刻狀態爲s_i的機率。app

綜上所述,一個隱馬爾科夫模型能夠用\lambda = [A,B,\pi]描述。機器學習

image

序列標籤

給訓練數據打上標籤,能夠指定四類標籤:BMES。其中 B 表明詞的開始,M 表明詞的中間,E 表明詞的結尾,S表明單字詞。好比下面

農B業E生B產E再B次E獲B得E好S的S收B成E
複製代碼

完成標籤後即獲得了觀察序列和狀態序列,隱馬爾科夫模型的五要素獲得了兩個,經過這兩個要素能夠將轉移機率矩陣、觀察機率矩陣和初始狀態機率計算出來。

實現代碼

先定義訓練文件讀取函數,這裏字與標籤之間以\t分割,讀取過程順便統計觀察對象集合和狀態集合。

def read_data(filename):
    sentences = []
    sentence = []
    with open(filename, 'r', encoding='utf-8') as f:
        for line in f.readlines():
            word_label = line.strip().split('\t')
            if len(word_label) == 2:
                observation_set.add(word_label[0])
                state_set.add(word_label[1])
                sentence.append(word_label)
            else:
                sentences.append(sentence)
                sentence = []
    return sentences
複製代碼

定義訓練函數,讀取訓練語料後遍歷全部句子,統計觀察機率矩陣observation_matrix,統計初始狀態機率pi_state,統計轉移機率矩陣transition_matrix,這裏咱們先統計個數,後面對其進行歸一化後獲得真正的機率。因此能夠看到後面分別對轉移機率矩陣、觀察機率矩陣和初始狀態作歸一化處理。

def train():
    print('begin training......')
    sentences = read_data(data_path)
    for sentence in sentences:
        pre_label = -1
        for word, label in sentence:
            observation_matrix[label][word] = observation_matrix.setdefault(label, {}).setdefault(word, 0) + 1
            if pre_label == -1:
                pi_state[label] = pi_state.setdefault(label, 0) + 1
            else:
                transition_matrix[pre_label][label] = transition_matrix.setdefault(pre_label, {}).setdefault(label,
                                                                                                             0) + 1
            pre_label = label

    for key, value in transition_matrix.items():
        number_total = 0
        for k, v in value.items():
            number_total += v
        for k, v in value.items():
            transition_matrix[key][k] = 1.0 * v / number_total
    for key, value in observation_matrix.items():
        number_total = 0
        for k, v in value.items():
            number_total += v
        for k, v in value.items():
            observation_matrix[key][k] = 1.0 * v / number_total

    number_total = sum(pi_state.values())
    for k, v in pi_state.items():
        pi_state[k] = 1.0 * v / number_total

    print('finish training.....')
    save_model()
複製代碼

訓練後將模型參數保存到文件中,預測時加載指定文件的模型。

def load_model():
    print('loading model...')
    with open(model_path, 'rb') as f:
        model = pickle.load(f)
    return model

def save_model():
    print('saving model...')
    model = [transition_matrix, observation_matrix, pi_state, state_set, observation_set]
    with open(model_path, 'wb') as f:
        pickle.dump(model, f)
複製代碼

訓練完成後獲得模型參數,接着定義預測函數,咱們預測其實就是 HMM 的解碼問題,通常可使用 Viterbi 算法,詳細能夠參考前面寫的文章《隱馬爾可夫模型的Viterbi解碼算法》。

隨着時刻推動,每一個節點都保存了前一時刻全部節點到該節點的最優值的子路徑,最優值經過上一時刻節點上的累乘機率乘以當前時刻機率來判斷的。當前時刻的某一節點可能的路徑爲上一時刻全部節點到該節點的路徑,但咱們只保留其中一條最優路徑,。依次計算完全部步後,最後經過回溯的方法獲得整個過程的最優路徑。

def predict():
    text = '我在圖書館看書'
    min_probability = -1 * float('inf')
    words = [{} for _ in text]
    path = {}
    for state in state_set:
        words[0][state] = 1.0 * pi_state.get(state, default_probability) * observation_matrix.get(state, {}).get(
            text[0],
            default_probability)
        path[state] = [state]
    for t in range(1, len(text)):
        new_path = {}
        for state in state_set:
            max_probability = min_probability
            max_state = ''
            for pre_state in state_set:
                probability = words[t - 1][pre_state] * transition_matrix.get(pre_state, {}).get(state,
                                                                                                 default_probability) \
                              * observation_matrix.get(state, {}).get(text[t], default_probability)
                max_probability, max_state = max((max_probability, max_state), (probability, pre_state))
            words[t][state] = max_probability
            tmp = copy.deepcopy(path[max_state])
            tmp.append(state)
            new_path[state] = tmp
        path = new_path
    max_probability, max_state = max((words[len(text) - 1][s], s) for s in state_set)
    result = []
    p = re.compile('BM*E|S')
    for i in p.finditer(''.join(path[max_state])):
        start, end = i.span()
        word = text[start:end]
        result.append(word)
    print(result)
複製代碼

大體描述下解碼過程,初始狀態時,對於四種狀態,「我」最大機率都是 S;接着分別計算「我」的四種狀態到「在」的四種狀態的機率,此時最大的也都爲 S;繼續分別計算「在」的四種狀態到「圖」的四種狀態的機率,最大的都爲 B;直到分別計算「館」的四種狀態到「看」的四種狀態的機率時,最大機率不一樣了;

'M': ['S']
'B': ['S']
'E': ['S']
'S': ['S']
複製代碼
......
複製代碼
'M': ['S', 'S', 'B', 'M', 'E', 'B', 'M']
'B': ['S', 'S', 'B', 'M', 'E', 'S', 'B']
'E': ['S', 'S', 'B', 'M', 'E', 'B', 'E']
'S': ['S', 'S', 'B', 'M', 'E', 'S', 'S']
複製代碼

github

github.com/sea-boat/nl…

-------------推薦閱讀------------

個人開源項目彙總(機器&深度學習、NLP、網絡IO、AIML、mysql協議、chatbot)

爲何寫《Tomcat內核設計剖析》

個人2017文章彙總——機器學習篇

個人2017文章彙總——Java及中間件

個人2017文章彙總——深度學習篇

個人2017文章彙總——JDK源碼篇

個人2017文章彙總——天然語言處理篇

個人2017文章彙總——Java併發篇


跟我交流,向我提問:

歡迎關注:

相關文章
相關標籤/搜索