从pytorch源码学BiLSTM+CRF

本文旨在通过pytorch源码理解CRF在NER中的实现,由于是源码的程序,更多讲的是公式的实现而不是具体的应用。

一、为什么要用CRF?

首先,句中的每个单词是一条包含词嵌入和字嵌入的词向量,词嵌入通常是事先训练好的,字嵌入则是随机初始化的。所有的嵌入都会随着训练的迭代过程被调整。其次,BiLSTM-CRF的输入是词嵌入向量,输出是每个单词对应的预测标签。即使没有CRF层,我们照样可以训练一个基于BiLSTM的命名实体识别模型。但是CRF层可以加入一些约束来保证最终预测结果是有效的。这些约束可以在训练数据时被CRF层自动学习得到。
可能的约束条件有:

  • 句子的开头应该是“B-”或“O”,而不是“I-”。
  • “B-label1 I-label2 I-label3…”,在该模式中,类别1,2,3应该是同一种实体类别。比如,“B-Person I-Person” 是正确的,而“B-Person I-Organization”则是错误的。
  • “O I-label”是错误的,命名实体的开头应该是“B-”而不是“I-”。
    有了这些有用的约束,错误的预测序列将会大大减少。


    CRF+BiLSTM模型

二、CRF如何融合BiLSTM中

BiLSTM 模型(加上一个线性层)的输出维度是tagset_size,这就相当于是每个词 x_i 映射到tagset=\{y_1,y_2,...,y_n\}的发射分数,设BiLSTM的输出矩阵为E,其中E_{i,j}代表词 x_i映射到y_j的非归一化分数。对于CRF来说,我们假定存在一个转移矩阵T,则T_{i,j}代表y_i转移到y_j的转移分数。
对于输入序列 X 对应的输出序列y,定义分数为:
s(X,y)=\sum_{i=0}^nT_{y_{i},y_{i+1}}+\sum_{i=0}^nE_{x_i,y_i}
利用Softmax函数,我们为每一个正确的序列y定义一个概率值(Y_X代表所有的y序列,包括不可能出现的)因而在训练中,我们只需要最大化似然概率p(y|X)即可,这里我们利用对数似然所以我们将损失函数定义为loss=-log(p(y|X)),就可以利用梯度下降法来进行网络的学习了。
loss = -log(p(y|X))=-log\frac{e^{s(X,y)}}{\sum_{\tilde{y}\in Y_X}e^{s(X,\tilde{y})}}=\log(\sum_{\tilde{y}\in Y_X}e^{S(X, \tilde{y})})-s(X,y)
损失函数主要包括两部分(1)真实路径的得分s(X,y)和所有可能路径的概率值\log(\sum_{\tilde{y}\in Y_X}e^{S(X, \tilde{y})})

三、CRF源码程序实现

程序的主体结构其实就是一个BiLSTM层接上一个Linear层self.hidden2tag,并且需要定义一个转移矩阵self.transitions,这里需要注意self.transitions的含义(self.transitions[i]表示所有可能的标签值到第i个标签的转移分数)

class BiLSTM_CRF(nn.Module):
    def __init__(self, vocab_size, tag_to_ix, embedding_dim, hidden_dim):
        super(BiLSTM_CRF, self).__init__()
        self.vocab_size = vocab_size
        self.tag_to_ix = tag_to_ix   # 包括了表示开始开始和结束的标签
        self.embedding_dim = embedding_dim
        self.hidden_dim = hidden_dim
        self.tagset_size = len(tag_to_ix)

        self.word_embeds = nn.Embedding(self.vocab_size, self.embedding_dim)
        self.BiLSTM = nn.LSTM(embedding_dim, hidden_size=hidden_dim//2,
                              bidirectional=True, num_layers=1)
        # hidden_size除以2是为了使BiLSTM的输出维度依然是hidden_size,而不用乘以2

        # 通过将BiLSTM的输出接上nn.Linear得到发射分数hidden2tag: [seq, 1, tagset_size]
        # batch_size在整个程序中是1
        self.hidden2tag = nn.Linear(hidden_dim, self.tagset_size)

        # CRF层学习的就是一个转移分数transitions: [tagset_size, tagset_size]
        # transitions[i]表示的是**从j=1,2,...,tagset_size**转移到第i个标签的分数
        # 而不能理解为**从第i个标签注意到j=1,2,...,tagset_size**的分数
        self.transitions = nn.Parameter(torch.randn([self.tagset_size, self.tagset_size]))

        # 用表示开始和结束的特殊字符找到需要识别的句子的开始和结束,用一个负无穷的数约束,这样exp
        # 一个负无穷的数,其得分就是0
        self.transitions.data[tag_to_ix[START_TAG], :] = -10000
        self.transitions.data[:, tag_to_ix[STOP_TAG]] = -10000

        # 初始化BiLSTM的隐层单元,可以不在这里初始化,因为forward函数内又写了一句
        self.hidden = self.init_hidden()

    def init_hidden(self):
        # 隐层单元的维度为:
        # [num_layers * num_directions, batch, hidden_size]
        return (torch.randn([2, 1, self.hidden_dim//2]),
                torch.randn([2, 1, self.hidden_dim//2]))

通过二中的分析可以知道,实现CRF的重点就是实现(1)真实路径的得分s(X,y)和所有可能路径的概率值\log(\sum_{\tilde{y}\in Y_X}e^{S(X, \tilde{y})})。首先实现s(X,y)中的发射分数。发射分数其实就是BiLSTM的输出接上一个线性层,线性层的维度就是tagset_size。

# 得到发射分数
    def _get_lstm_features(self, sentence):
        self.hidden = self.init_hidden()
        embeds = self.word_embeds(sentence).view([len(sentence), 1, -1])
        BiLSTM_out, self.hidden = self.BiLSTM(embeds, self.hidden)
        # BiLSTM_out的输出本来是[seq_len, batch_size, hidden_dim]
        # 由于batch_size为1,view成了二维的
        BiLSTM_out = BiLSTM_out.view([len(sentence), self.hidden_dim])
        BiLSTM_feats = self.hidden2tag(BiLSTM_out)
        return BiLSTM_feats

其次,实现较为简单的s(X,y),细节的地方直接看程序就行,程序中都注释的很清楚了。

    # 得到正确路径的分数,即公式中的S(X, y)
    # 只需要:
    # (1)依次得到标签tags对应的转移分数
    # (2)加上feats对应的发射分数就行了
    def _score_sentence(self, feats, tags):
        # feats: [seq_len, tagset_size]
        # tags:  [tagset_size]
        score = torch.zeros([1])
        # 由于tags里不含表示开始和结束的特殊字符,而转移分数的矩阵内是有的
        # 因此首先在tags添加了表示开始的特殊字符
        tags = torch.cat([torch.tensor([self.tag_to_ix[START_TAG]],
                                       dtype=torch.long), tags])
        for i, feat in enumerate(feats):
            # transitions[tags[i+1], tags[i]]表示第i个标签转移到第i+1个标签的转移分数
            score = score + self.transitions[tags[i+1], tags[i]] + feat[tags[i+1]]
        score = score + self.transitions[self.tag_to_ix[STOP_TAG], tags[-1]]
        return score

接着实现复杂的\log(\sum_{\tilde{y}\in Y_X}e^{S(X, \tilde{y})})。这里采用动态规划的思想,强烈推荐知乎CSDN上写的,程序中的相关注释请看程序中写的。

    # 得到所有路径的分数,即公式中的$\log\sum_{\tilde{y}\in Y_X}e^{S(X, \tilde{y})}$
    # 这里采用类似动态规划的做法,因为要求解出所有可能路径的分数再求和时间复杂度太高了
    # 可以依次从前往后计算出每一条路径上的分数,当计算下一条路径时,直接加上前一次计算得到的路径分数就行
    # 因此在前向计算的过程中需要保存前一次计算的路径得分,程序中用forward_var表示的,维度为:[1 tagset_size]
    def _forward_alg(self, feats):
        # feats: [seq_len, tagset_size]
        # 初始化forward_var, 并且开始位置的分数为0, 迫使转移矩阵学到START_TAG的得分最高
        forward_var = torch.full([1, self.tagset_size], -10000.)
        forward_var[0][self.tag_to_ix[START_TAG]] = 0.

        # 前向过程计算分数的前向是针对seq_len而言的,每一次存储都是在每一次seq_len的结束存储的
        for feat in feats:
            # forward_var_t表示每一次前向过程中的分数
            # forward_var_t与forward_var不同,forward_var_t每一次前向过程中需要更新,但是
            # forward_var是累加的
            forward_var_t = []
            # 这个for循环计算的是在t时刻前向计算过程中,所有标签到某个具体标签的得分
            #   x0   |  x1    x2    x3
            # ------>---->----->----->------->
            #  START | START START START
            #   y1   |  y1    y1    y1
            #   y2   |  y2    y2    y2
            #   y3   |  y3    y3    y3
            #  STOP  | STOP  STOP  STOP
            # 假设feats = [[x1, x2, x3]]
            # 可能的标签为{START, y1, y2, y3, STOP}
            # 假如此时feat = x1
            # 则此时下面的for循环需要依次计算:
            # (1) {START, y1, y2, y3, STOP}到START的总分数作为forward的**第0个元素**
            # (2) {START, y1, y2, y3, STOP}到y1的总分数作为forward的*****第1个元素**
            # ............................................................
            # (5) {START, y1, y2, y3, STOP}到STOP的总分数作为forward的***第4个元素**
            # ======================== 细节: 如何采用动态规划思想 ================
            # 对于计算(2) **{START, y1, y2, y3, STOP}到y1的总分数作为forward的第1个元素** 时
            # 需要分别加上forward在前一时刻(t-1时刻)的得分,举例: 计算START到y1的分数S(START, y1):
            # S(START, y1) = forward[0] + E(x1, y1) + T(START, y1)
            # 其中forward[0]表示t-1时刻所有到达START所有路径的得分
            # E(x1, y1)与T(START, y1)分别表示发射分数和转移分数
            for next_tag in range(self.tagset_size):
                # 复制emit_score的目的是因为对于t-1时刻无论何种方式到达标签next_tag,其对应的发射分数不会变
                # 变的是转移分数
                emit_score = feat[next_tag].view([1, -1]).expand([1, self.tagset_size])
                trans_score = self.transitions[next_tag].view([1, -1])
                # ===================== 这里计算的就是前面说的细节处的计算=======================
                next_tag_var = forward_var + trans_score + emit_score
                forward_var_t.append(log_sum_exp(next_tag_var).view([1]))
            forward_var = torch.cat(forward_var_t).view([1, -1])
        # 计算最后到达STOP_END的得分,此时只有转移分数
        forward_var += self.transitions[self.tag_to_ix[STOP_TAG]]
        forward_var = log_sum_exp(forward_var)
        return forward_var

最后实现的就是损失函数,损失函数的实现其实就是将前两步中的公式相减。

    # 计算CRF的损失函数
    # $ Loss = -(S(X, y) - \log\sum_{\tilde{y}\in Y_X}e^{S(X, \tilde{y})} $
    #        = self._forward_alg(feats) - self._score_sentence(feats, tags)
    def neg_log_likelihood(self, sentence, tags):
        feats = self._get_lstm_features(sentence)
        forward_score = self._forward_alg(feats)
        gold_score = self._score_sentence(feats, tags)
        return forward_score - gold_score

四:采用维特比算法解码

    # viterbi解码时,也是运用了动态规划的思想,其实和self._forward_alg类似,
    def _viterbi_decode(self, feats):
        # 初始化forward_var,并且开始位置的分数为0,确保一定是从START_TAG开始的,
        # 因为 $e^{-10000}<<e^0$
        forward_var = torch.full([1, self.tagset_size], -10000.)
        forward_var[0][self.tag_to_ix[START_TAG]] = 0
        # backpointers用来计算每一个时刻每一个标签对应的最佳路径
        backpointers = []
        for feat in feats:
            backpointers_t = []  # t时刻的最佳路径
            forward_var_t = []  # t时刻的最佳路径的得分
            #   x0   |  x1    x2    x3
            # ------>---->----->----->------->
            #  START | START START START
            #   y1   |  y1    y1    y1
            #   y2   |  y2    y2    y2
            #   y3   |  y3    y3    y3
            #  STOP  | STOP  STOP  STOP
            # 当feat=x2时,假如在t-1时刻START到{START, y1, y2, y3, STOP}的路径得分最大
            # 此时,需要求t时刻达到{START, y1, y2, y3, STOP}的路径得分
            # 由于此时发射分数都是一样的,因此只要比较转移分数就行
            # 举例:计算{START, y1, y2, y3, STOP}到START的最大路径
            # 计算max(T(START, START)+forward_var[0], T(y1, START)+forward_var[1], T(y2, START), ...)
            # 假设T(y1, START)+forward_var[1]最大,此时y1对应的索引(也就是1)被记录在backpointers_t中,
            # 值T(y1, START)+forward_var[1]+E(x2, y1)被记录在forward_var_t中
            for next_tag in range(self.tagset_size):
                next_tag_var = forward_var + self.transitions[next_tag]
                best_tag_id = argmax(next_tag_var)
                backpointers_t.append(best_tag_id)
                forward_var_t.append(next_tag_var[0][best_tag_id].view([1]))
            # 更新forward_var
            forward_var = (torch.cat(forward_var_t) + feat).view([1, -1])
            # 添加backpointers
            backpointers.append(backpointers_t)

        # 计算到STOP_TAG的最优路径,其得分也就是最优路径的得分
        forward_var += self.transitions[self.tag_to_ix[STOP_TAG]]
        best_tag_id = argmax(forward_var)
        path_score = forward_var[0][best_tag_id]

        # 通过backpointers逆序找到最佳路径
        best_path = [best_tag_id]
        for backpointers_t in reversed(backpointers):
            best_tag_id = backpointers_t[best_tag_id]
            best_path.append(best_tag_id)
        # 弹出START_TAG
        start = best_path.pop()
        assert self.tag_to_ix[START_TAG] == start
        best_path.reverse()
        return path_score, best_path

五、所有程序的实现和注释

# -*- coding: utf-8 -*-
import torch
import torch.autograd as autograd
import torch.nn as nn
import torch.optim as optim

torch.manual_seed(1)

# ========================= 相关函数 ==========================
def argmax(vec):
    # 得到最大值的标签
    # vec: [1, tagset_size]
    _, idx = torch.max(vec, 1)
    return idx.item()

def prepare_sequence(seq, to_ix):
    # 将一句话中的字转换为索引
    # seq: [vocab_size]
    # to_ix: dict
    idxs = [to_ix[word] for word in seq]
    return torch.tensor(idxs, dtype=torch.long)

def log_sum_exp(vec):
    # 计算$\log\sum{e^{vec_i}}$
    # vec: [1, tagset_size]
    # 为防止指数运算溢出,先将vec中元素减去最大值,最后在结果中加上最大值
    max_score = vec[0, argmax(vec)]
    # max_score的维度可以不扩展,直接利用广播机制也行
    max_score_broadcast = max_score.view([1, -1]).expand([1, vec.size()[1]])
    # 最后return返回的是一个一维的tensor
    return max_score + torch.log(torch.sum(torch.exp(vec - max_score_broadcast)))

# ====================== BILSTM实现 ==================================
class BiLSTM_CRF(nn.Module):
    def __init__(self, vocab_size, tag_to_ix, embedding_dim, hidden_dim):
        super(BiLSTM_CRF, self).__init__()
        self.vocab_size = vocab_size
        self.tag_to_ix = tag_to_ix   # 包括了表示开始开始和结束的标签
        self.embedding_dim = embedding_dim
        self.hidden_dim = hidden_dim
        self.tagset_size = len(tag_to_ix)

        self.word_embeds = nn.Embedding(self.vocab_size, self.embedding_dim)
        self.BiLSTM = nn.LSTM(embedding_dim, hidden_size=hidden_dim//2,
                              bidirectional=True, num_layers=1)
        # hidden_size除以2是为了使BiLSTM的输出维度依然是hidden_size,而不用乘以2

        # 通过将BiLSTM的输出接上nn.Linear得到发射分数hidden2tag: [seq, 1, tagset_size]
        # batch_size在整个程序中的维度是1
        self.hidden2tag = nn.Linear(hidden_dim, self.tagset_size)

        # CRF层学习的就是一个转移分数transitions: [tagset_size, tagset_size]
        # transitions[i]表示的是**从j=1,2,...,tagset_size**转移到第i个标签的分数
        # 而不能理解为**从第i个标签注意到j=1,2,...,tagset_size**的分数
        self.transitions = nn.Parameter(torch.randn([self.tagset_size, self.tagset_size]))

        # 用表示开始和结束的特殊字符找到需要识别的句子的开始和结束,用一个负无穷的数约束,这样exp
        # 一个负无穷的数,其得分就是0
        self.transitions.data[tag_to_ix[START_TAG], :] = -10000
        self.transitions.data[:, tag_to_ix[STOP_TAG]] = -10000

        # 初始化BiLSTM的隐层单元,可以不在这里初始化,因为forward函数内又写了一句
        self.hidden = self.init_hidden()

    def init_hidden(self):
        # 隐层单元的维度为:
        # [num_layers * num_directions, batch, hidden_size]
        return (torch.randn([2, 1, self.hidden_dim//2]),
                torch.randn([2, 1, self.hidden_dim//2]))

    # 得到发射分数
    def _get_lstm_features(self, sentence):
        self.hidden = self.init_hidden()
        embeds = self.word_embeds(sentence).view([len(sentence), 1, -1])
        BiLSTM_out, self.hidden = self.BiLSTM(embeds, self.hidden)
        # BiLSTM_out的输出本来是[seq_len, batch_size, hidden_dim]
        # 由于batch_size为1,view成了二维的
        BiLSTM_out = BiLSTM_out.view([len(sentence), self.hidden_dim])
        BiLSTM_feats = self.hidden2tag(BiLSTM_out)
        return BiLSTM_feats

    # 得到正确路径的分数,即公式中的S(X, y)
    # 只需要:
    # (1)依次得到标签tags对应的转移分数
    # (2)加上feats对应的发射分数就行了
    def _score_sentence(self, feats, tags):
        # feats: [seq_len, tagset_size]
        # tags:  [tagset_size]
        score = torch.zeros([1])
        # 由于tags里不含表示开始和结束的特殊字符,而转移分数的矩阵内是有的
        # 因此首先在tags添加了表示开始的特殊字符
        tags = torch.cat([torch.tensor([self.tag_to_ix[START_TAG]],
                                       dtype=torch.long), tags])
        for i, feat in enumerate(feats):
            # transitions[tags[i+1], tags[i]]表示第i个标签转移到第i+1个标签的转移分数
            score = score + self.transitions[tags[i+1], tags[i]] + feat[tags[i+1]]
        score = score + self.transitions[self.tag_to_ix[STOP_TAG], tags[-1]]
        return score

    # 得到所有路径的分数,即公式中的$\log\sum_{\tilde{y}\in Y_X}e^{S(X, \tilde{y})}$
    # 这里采用类似动态规划的做法,因为要求解出所有可能路径的分数再求和时间复杂度太高了
    # 可以依次从前往后计算出每一条路径上的分数,当计算下一条路径时,直接加上前一次计算得到的路径分数就行
    # 因此在前向计算的过程中需要保存前一次计算的路径得分,程序中用forward_var表示的,维度为:[1 tagset_size]
    def _forward_alg(self, feats):
        # feats: [seq_len, tagset_size]
        # 初始化forward_var, 并且开始位置的分数为0, 迫使转移矩阵学到START_TAG的得分最高
        forward_var = torch.full([1, self.tagset_size], -10000.)
        forward_var[0][self.tag_to_ix[START_TAG]] = 0.

        # 前向过程计算分数的前向是针对seq_len而言的,每一次存储都是在每一次seq_len的结束存储的
        for feat in feats:
            # forward_var_t表示每一次前向过程中的分数
            # forward_var_t与forward_var不同,forward_var_t每一次前向过程中需要更新,但是
            # forward_var是累加的
            forward_var_t = []
            # 这个for循环计算的是在t时刻前向计算过程中,所有标签到某个具体标签的得分
            #   x0   |  x1    x2    x3
            # ------>---->----->----->------->
            #  START | START START START
            #   y1   |  y1    y1    y1
            #   y2   |  y2    y2    y2
            #   y3   |  y3    y3    y3
            #  STOP  | STOP  STOP  STOP
            # 假设feats = [[x1, x2, x3]]
            # 可能的标签为{START, y1, y2, y3, STOP}
            # 假如此时feat = x1
            # 则此时下面的for循环需要依次计算:
            # (1) {START, y1, y2, y3, STOP}到START的总分数作为forward的**第0个元素**
            # (2) {START, y1, y2, y3, STOP}到y1的总分数作为forward的*****第1个元素**
            # ............................................................
            # (5) {START, y1, y2, y3, STOP}到STOP的总分数作为forward的***第4个元素**
            # ======================== 细节: 如何采用动态规划思想 ================
            # 对于计算(2) **{START, y1, y2, y3, STOP}到y1的总分数作为forward的第1个元素** 时
            # 需要分别加上forward在前一时刻(t-1时刻)的得分,举例: 计算START到y1的分数S(START, y1):
            # S(START, y1) = forward[0] + E(x1, y1) + T(START, y1)
            # 其中forward[0]表示t-1时刻所有到达START所有路径的得分
            # E(x1, y1)与T(START, y1)分别表示发射分数和转移分数
            for next_tag in range(self.tagset_size):
                # 复制emit_score的目的是因为对于t-1时刻无论何种方式到达标签next_tag,其对应的发射分数不会变
                # 变的是转移分数
                emit_score = feat[next_tag].view([1, -1]).expand([1, self.tagset_size])
                trans_score = self.transitions[next_tag].view([1, -1])
                # ===================== 这里计算的就是前面说的细节处的计算=======================
                next_tag_var = forward_var + trans_score + emit_score
                forward_var_t.append(log_sum_exp(next_tag_var).view([1]))
            forward_var = torch.cat(forward_var_t).view([1, -1])
        # 计算最后到达STOP_END的得分,此时只有转移分数
        forward_var += self.transitions[self.tag_to_ix[STOP_TAG]]
        forward_var = log_sum_exp(forward_var)
        return forward_var

    # 计算CRF的损失函数
    # $ Loss = -(S(X, y) - \log\sum_{\tilde{y}\in Y_X}e^{S(X, \tilde{y})} $
    #        = self._forward_alg(feats) - self._score_sentence(feats, tags)
    def neg_log_likelihood(self, sentence, tags):
        feats = self._get_lstm_features(sentence)
        forward_score = self._forward_alg(feats)
        gold_score = self._score_sentence(feats, tags)
        return forward_score - gold_score

    # viterbi解码时,也是运用了动态规划的思想,其实和self._forward_alg类似,
    def _viterbi_decode(self, feats):
        # 初始化forward_var,并且开始位置的分数为0,确保一定是从START_TAG开始的,
        # 因为 $e^{-10000}<<e^0$
        forward_var = torch.full([1, self.tagset_size], -10000.)
        forward_var[0][self.tag_to_ix[START_TAG]] = 0
        # backpointers用来计算每一个时刻每一个标签对应的最佳路径
        backpointers = []
        for feat in feats:
            backpointers_t = []  # t时刻的最佳路径
            forward_var_t = []  # t时刻的最佳路径的得分
            #   x0   |  x1    x2    x3
            # ------>---->----->----->------->
            #  START | START START START
            #   y1   |  y1    y1    y1
            #   y2   |  y2    y2    y2
            #   y3   |  y3    y3    y3
            #  STOP  | STOP  STOP  STOP
            # 当feat=x2时,假如在t-1时刻START到{START, y1, y2, y3, STOP}的路径得分最大
            # 此时,需要求t时刻达到{START, y1, y2, y3, STOP}的路径得分
            # 由于此时发射分数都是一样的,因此只要比较转移分数就行
            # 举例:计算{START, y1, y2, y3, STOP}到START的最大路径
            # 计算max(T(START, START)+forward_var[0], T(y1, START)+forward_var[1], T(y2, START), ...)
            # 假设T(y1, START)+forward_var[1]最大,此时y1对应的索引(也就是1)被记录在backpointers_t中,
            # 值T(y1, START)+forward_var[1]+E(x2, y1)被记录在forward_var_t中
            for next_tag in range(self.tagset_size):
                next_tag_var = forward_var + self.transitions[next_tag]
                best_tag_id = argmax(next_tag_var)
                backpointers_t.append(best_tag_id)
                forward_var_t.append(next_tag_var[0][best_tag_id].view([1]))
            # 更新forward_var
            forward_var = (torch.cat(forward_var_t) + feat).view([1, -1])
            # 添加backpointers
            backpointers.append(backpointers_t)

        # 计算到STOP_TAG的最优路径,其得分也就是最优路径的得分
        forward_var += self.transitions[self.tag_to_ix[STOP_TAG]]
        best_tag_id = argmax(forward_var)
        path_score = forward_var[0][best_tag_id]

        # 通过backpointers逆序找到最佳路径
        best_path = [best_tag_id]
        for backpointers_t in reversed(backpointers):
            best_tag_id = backpointers_t[best_tag_id]
            best_path.append(best_tag_id)
        # 弹出START_TAG
        start = best_path.pop()
        assert self.tag_to_ix[START_TAG] == start
        best_path.reverse()
        return path_score, best_path

    def forward(self, sentence):
        # 得到发射分数
        BiLSTM_feats = self._get_lstm_features(sentence)

        # 通过viterbi找出最佳路径
        score, best_path = self._viterbi_decode(BiLSTM_feats)
        return score, best_path

if __name__ == '__main__':
    START_TAG = "<START>"
    STOP_TAG = "<STOP>"
    EMBEDDING_DIM = 5
    HIDDEN_DIM = 4

    training_data = [(
        "the wall street journal reported today that apple corporation made money".split(),
        "B I I I O O O B I O O".split()
    ), (
        "georgia tech is a university in georgia".split(),
        "B I O O O O B".split()
    )]

    word_to_ix = {}
    for sentence, tags in training_data:
        for word in sentence:
            if word_to_ix.get(word) is None:
                word_to_ix[word] = len(word_to_ix)

    tag_to_ix = {"B": 0, "I": 1, "O": 2, START_TAG: 3, STOP_TAG: 4}
    model = BiLSTM_CRF(vocab_size=len(word_to_ix),
                       tag_to_ix=tag_to_ix,
                       embedding_dim=EMBEDDING_DIM,
                       hidden_dim=HIDDEN_DIM, )
    optimizer = optim.SGD(model.parameters(), lr=0.01, weight_decay=1e-4)

    # 仅仅为了print
    with torch.no_grad():
        precheck_sent = prepare_sequence(training_data[0][0], word_to_ix)
        precheck_tags = torch.tensor([tag_to_ix[t] for t in training_data[0][1]], dtype=torch.long)
        print(model(precheck_sent))

    # 开始训练
    for epoch in range(10):
        # 注意: 这里的batch_size为1
        for sentence, tags in training_data:
            model.zero_grad()
            sentence_in = prepare_sequence(sentence, word_to_ix)
            targets = torch.tensor([tag_to_ix[t] for t in tags], dtype=torch.long)

            loss = model.neg_log_likelihood(sentence_in, targets)
            loss.backward()
            optimizer.step()

    # 预测
    with torch.no_grad():
        precheck_sent = prepare_sequence(training_data[0][0], word_to_ix)
        print(model(precheck_sent))
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342