本文是對Pytorch官方文件中的示例程式碼進行的說明。希望透過這篇文章,能讓自己對這個模型程式碼的理解更加透徹。

本文適合ner或者說深度學習小白食用~

本文食用方法

:程式碼,參考文件1,都要看,參考文件2、3僅作為補充。後面附的程式碼除了維特比函式之外幾乎每一句都有解析,也可以看完參考文件1後不懂的地方直接看本文。

先說一下我學習這份程式碼的過程:找參考文件看,瞭解原理->下載原始碼跑通->對著程式碼一行一行看,看懂語法->開始在網上找各種原始碼解析資料,再次看程式碼,看懂每個函式的輸入輸出分別是什麼->第三遍看程式碼,分析函式中每一個用到的引數分別是什麼含義,在紙上把每一個張量用影象畫一下->最後一遍看程式碼,總結所有函式互相呼叫的過程,邊看邊畫圖(不過這次畫的圖太抽象了,很多細節都不在上面,大家就當作函式目錄看一下好啦),並根據自己理解註釋程式碼->形成這篇文字。

一共歷時將近2個月(不過前一個月多在家過暑假來著全把時間放在語法上了,自律性差,搞了太久),從第三遍用了4天,第四遍只用了1個下午。這是我搞的差不多的第一個深度學習模型,希望有了這次經歷以後學習起來可以輕鬆一些。(其實還有很多細節沒有掌握,以後慢慢來啦哈哈哈)

程式碼來源

https://

pytorch。org/tutorials/b

eginner/nlp/advanced_tutorial。html

參考文件

:參考文件1 這篇文章裡給了很多理論解釋的連結,建議先看這篇打好理論基礎。

參考文件2 這篇文章裡給出了很多公式的變形過程,建議瞭解大致工作原理,在對照程式碼看的時候看這篇哦,還要拿好紙和筆跟著演算(比如 為什麼句子總分數用了log_sum_exp()計算而實際句子分數卻不用)。

參考文件3 這篇文章的作者對程式碼很細節的地方都進行了解析!非常易懂,如果有程式碼細節搞不明白的地方(小白搞不明白的地方哈哈,因為我的每一個搞不懂都透露著我是小白本白)可以看這篇哦。

我畫der圖:

BiLSTM-CRF實現NER程式碼逐行解析-小白版

import

torch

import

torch。autograd

as

autograd

import

torch。nn

as

nn

import

torch。optim

as

optim

#進行最佳化演算法時使用

torch

manual_seed

1

#設定隨機種子,使得每次使用rand生成的隨機數是一樣的

‘’‘

以數值形式返回張量最大值的索引

輸入:vec: 張量,由下面的引用可知這裡應該是一維張量(只有一行),即向量

輸出:以值的形式返回vec中每行最大值的索引

’‘’

def

argmax

vec

):

# return the argmax as a python int

_

idx

=

torch

max

vec

1

return

idx

item

()

‘’‘

根據輸入的句子序列轉換成在字典中對應的序號序列

用於對輸入的句子進行預處理

輸入:seq: 用於訓練的句子序列集

to_ix: 陣列,從seq中單詞對映到id

輸出:句子序列對應的id tensor(張量)

’‘’

def

prepare_sequence

seq

to_ix

):

idxs

=

to_ix

w

for

w

in

seq

return

torch

tensor

idxs

dtype

=

torch

long

# Compute log sum exp in a numerically stable way for the forward algorithm

‘’‘

用於計算句子的總得分

輸入:vec: 綜合分數的張量(還不清楚這個向量是幾維,如何計算的)

輸出:損失函式,用於向前計算,訓練時使用

’‘’

def

log_sum_exp

vec

):

max_score

=

vec

0

argmax

vec

)]

max_score_broadcast

=

max_score

view

1

-

1

expand

1

vec

size

()[

1

])

return

max_score

+

\

torch

log

torch

sum

torch

exp

vec

-

max_score_broadcast

)))

‘’‘建立模型時使用的類’‘’

class

BiLSTM_CRF

nn

Module

):

‘’‘

初始化函式

輸入:字典大小,標籤-id對應陣列,詞嵌入向量的維度,隱層的維度

’‘’

def

__init__

self

vocab_size

tag_to_ix

embedding_dim

hidden_dim

):

super

BiLSTM_CRF

self

__init__

()

self

embedding_dim

=

embedding_dim

self

hidden_dim

=

hidden_dim

self

vocab_size

=

vocab_size

self

tag_to_ix

=

tag_to_ix

self

tagset_size

=

len

tag_to_ix

#tagset_size 指tag的個數

self

word_embeds

=

nn

Embedding

vocab_size

embedding_dim

#利用已有的Embedding函式建立嵌入層(該層維度:vocab_size,embedding_dim)

self

lstm

=

nn

LSTM

embedding_dim

hidden_dim

//

2

num_layers

=

1

bidirectional

=

True

#利用已有的LSTM函式建立雙向lstm層,

#特別注意一下這裡的//2(因為是雙向的關係)

# Maps the output of the LSTM into tag space。

self

hidden2tag

=

nn

Linear

hidden_dim

self

tagset_size

#用線性函式,將隱層(lstm)的輸出張量對映到tag域的輸出層

# Matrix of transition parameters。 Entry i,j is the score of

# transitioning *to* i *from* j。

self

transitions

=

nn

Parameter

#使用Parameter()得到的引數可以自動隨著模型發生改變

torch

randn

self

tagset_size

self

tagset_size

))

#轉移矩陣的初始化(維度:tagset_size,tagset_size)

# These two statements enforce the constraint that we never transfer

# to the start tag and we never transfer from the stop tag

#轉移矩陣是為了體現標籤之間相互轉移的關係,注意:transitions[i][j] 表示從j->i的可能分數

self

transitions

data

tag_to_ix

START_TAG

],

:]

=

-

10000

#不能從任何標籤轉移到START_TAG

self

transitions

data

[:,

tag_to_ix

STOP_TAG

]]

=

-

10000

#STOP_TAG不能轉移到任何標籤

self

hidden

=

self

init_hidden

()

‘’‘

初始化隱層的引數h0(每個句子的初始隱藏狀態)與c0(每個句子的初始細胞狀態),這裡的隱層就是Bilstm層

輸出:初始化的隱層引數(張量維度:num_layers*num_directions,batch_size,hidden_size)

’‘’

def

init_hidden

self

):

return

torch

randn

2

1

self

hidden_dim

//

2

),

torch

randn

2

1

self

hidden_dim

//

2

))

‘’‘

計算一個句子的所有路徑總得分,要注意這裡的 得分 不是簡單的幾個矩陣分數對應相加,是以log_sum_exp形式得到的,是公式中的分母

輸入:feats: lstm層的發射矩陣(張量維度:seq_len, tagset_size) ?????

輸出:所有路徑的 總得分(是根據公式來的)

’‘’

def

_forward_alg

self

feats

):

# Do the forward algorithm to compute the partition function

init_alphas

=

torch

full

((

1

self

tagset_size

),

-

10000。

#init_alphas 用於向前計算訓練時的引數(維度:1,tagset_size)。這裡是用-10000對引數初始化

# START_TAG has all of the score。

init_alphas

0

][

self

tag_to_ix

START_TAG

]]

=

0。

#一定是從 START_TAG 轉移到該句子第一個單詞的

# Wrap in a variable so that we will get automatic backprop

forward_var

=

init_alphas

#具體運算的過程可以參考公式

# Iterate through the sentence

for

feat

in

feats

#feat: 每一個單詞(時間步)對應的各個tag得分

alphas_t

=

[]

# The forward tensors at this timestep 當前時間步的向前函式

for

next_tag

in

range

self

tagset_size

):

# broadcast the emission score: it is the same regardless ofthe previous tag

emit_score

=

feat

next_tag

view

1

-

1

expand

1

self

tagset_size

#看到這裡可以明白,feats就是lstm層的發射矩陣

# the ith entry of trans_score is the score of transitioning to

# next_tag from i

trans_score

=

self

transitions

next_tag

view

1

-

1

# The ith entry of next_tag_var is the value for the

# edge (i -> next_tag) before we do log-sum-exp

next_tag_var

=

forward_var

+

trans_score

+

emit_score

#下一個tag時當前next_tag的得分為三個分數加和

# The forward variable for this tag is log-sum-exp of all the

# scores。

alphas_t

append

log_sum_exp

next_tag_var

view

1

))

#next_tag的綜合分數

forward_var

=

torch

cat

alphas_t

view

1

-

1

#更新句子的向前引數

terminal_var

=

forward_var

+

self

transitions

self

tag_to_ix

STOP_TAG

]]

#算上從句子最後一個單詞轉移到STOP_TAG的分數

alpha

=

log_sum_exp

terminal_var

#得到最終得分

return

alpha

‘’‘

得到一個句子中每個詞的lstm輸出對應的tag(即lstm層的發射矩陣),也就是經過lstm層得到的分數

輸入:sentence:一個句子

輸出:在tag中的對映,即lstm層的發射矩陣(張量維度:seq_len, tagset)

’‘’

def

_get_lstm_features

self

sentence

):

self

hidden

=

self

init_hidden

()

embeds

=

self

word_embeds

sentence

view

len

sentence

),

1

-

1

#改變維度為(seq_len,batch_size,embedding_dim)

lstm_out

self

hidden

=

self

lstm

embeds

self

hidden

#更新隱層引數

lstm_out

=

lstm_out

view

len

sentence

),

self

hidden_dim

#LSTM輸出是二維向量

lstm_feats

=

self

hidden2tag

lstm_out

#線性變化,對映到tag_space,作為發射矩陣

return

lstm_feats

‘’‘

給出lstm層得到的句子真實標籤的訓練得分(這裡的得分是不需要用到log_sum_exp()函式的,因為對公式進行了變形)

輸入:feats:lstm層發射矩陣

tags: 給出的標記序列

輸出:句子的得分 = 轉移分數+lstm層的計算分數

’‘’

def

_score_sentence

self

feats

tags

):

# Gives the score of a provided tag sequence

score

=

torch

zeros

1

tags

=

torch

cat

([

torch

tensor

([

self

tag_to_ix

START_TAG

]],

dtype

=

torch

long

),

tags

])

#在給定句子的開頭加START_TAG(cat()中括號和小括號都可以)

for

i

feat

in

enumerate

feats

):

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

]]

#給句子算上轉移到最後STOP_TAG的分數

return

score

‘’‘

用於計算最佳路徑以及最佳路徑得分

輸入引數:feats: 句子到tag的對映張量

輸出引數:(預測的)最佳路徑的得分;最佳路徑

’‘’

#維特比解碼器

def

_viterbi_decode

self

feats

):

backpointers

=

[]

# Initialize the viterbi variables in log space

init_vvars

=

torch

full

((

1

self

tagset_size

),

-

10000。

init_vvars

0

][

self

tag_to_ix

START_TAG

]]

=

0

# forward_var at step i holds the viterbi variables for step i-1

forward_var

=

init_vvars

for

feat

in

feats

bptrs_t

=

[]

# holds the backpointers for this step

viterbivars_t

=

[]

# holds the viterbi variables for this step

for

next_tag

in

range

self

tagset_size

):

# next_tag_var[i] holds the viterbi variable for tag i at the

# previous step, plus the score of transitioning

# from tag i to next_tag。

# We don‘t include the emission scores here because the max

# does not depend on them (we add them in below)

next_tag_var

=

forward_var

+

self

transitions

next_tag

best_tag_id

=

argmax

next_tag_var

bptrs_t

append

best_tag_id

viterbivars_t

append

next_tag_var

0

][

best_tag_id

view

1

))

# Now add in the emission scores, and assign forward_var to the set

# of viterbi variables we just computed

forward_var

=

torch

cat

viterbivars_t

+

feat

view

1

-

1

backpointers

append

bptrs_t

# Transition to STOP_TAG

terminal_var

=

forward_var

+

self

transitions

self

tag_to_ix

STOP_TAG

]]

best_tag_id

=

argmax

terminal_var

path_score

=

terminal_var

0

][

best_tag_id

# Follow the back pointers to decode the best path。

best_path

=

best_tag_id

for

bptrs_t

in

reversed

backpointers

):

best_tag_id

=

bptrs_t

best_tag_id

best_path

append

best_tag_id

# Pop off the start tag (we dont want to return that to the caller)

start

=

best_path

pop

()

assert

start

==

self

tag_to_ix

START_TAG

# Sanity check

best_path

reverse

()

#將得到的路徑反轉得到真實的最佳路徑

return

path_score

best_path

#反向傳播

’‘’

綜合上面所寫的計算真實的路徑值,和計算路徑值之和的函式,用二者之差作為loss,我們的目標是透過訓練讓loss變小

loss越小,說明非正確路徑的得分越接近0,結果也就越準確

輸入:sentence:所用句子

tags:真實的序列標籤。

輸出:loss

‘’‘

def

neg_log_likelihood

self

sentence

tags

):

feats

=

self

_get_lstm_features

sentence

#lstm

forward_score

=

self

_forward_alg

feats

#前向傳播,算出的所有路徑的總分數

gold_score

=

self

_score_sentence

feats

tags

#根據實際得到的標籤計算的真實路徑分數

return

forward_score

-

gold_score

#根據差值反向訓練,損失函式

’‘’

應該是用於預測的函式(雖然不知道為啥叫 forward)

輸入:sentence:待預測的句子序列

輸出:tag序列得分,tag序列

‘’‘

def

forward

self

sentence

):

# dont confuse this with _forward_alg above。

# Get the emission scores from the BiLSTM

lstm_feats

=

self

_get_lstm_features

sentence

# Find the best path, given the features。

score

tag_seq

=

self

_viterbi_decode

lstm_feats

return

score

tag_seq

START_TAG

=

STOP_TAG

=

EMBEDDING_DIM

=

5

HIDDEN_DIM

=

4

# Make up some training data

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

()

)]

test_data

=

[(

“the apple”

split

(),

“B I”

split

()

)]

word_to_ix

=

{}

for

sentence

tags

in

training_data

for

word

in

sentence

if

word

not

in

word_to_ix

word_to_ix

word

=

len

word_to_ix

tag_to_ix

=

{

“B”

0

“I”

1

“O”

2

START_TAG

3

STOP_TAG

4

}

ix_to_tag

=

{}

for

k

v

in

tag_to_ix

items

():

ix_to_tag

v

=

k

model

=

BiLSTM_CRF

len

word_to_ix

),

tag_to_ix

EMBEDDING_DIM

HIDDEN_DIM

optimizer

=

optim

SGD

model

parameters

(),

lr

=

0。01

weight_decay

=

1e-4

#最佳化器

# Check predictions before training

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

))

tensor

9。2679

),

1

2

2

2

2

2

2

2

2

2

2

])

# Make sure prepare_sequence from earlier in the LSTM section is loaded

for

epoch

in

range

300

):

# again, normally you would NOT do 300 epochs, it is toy data

for

sentence

tags

in

training_data

# Step 1。 Remember that Pytorch accumulates gradients。

# We need to clear them out before each instance

model

zero_grad

()

#對於每一條句子都應該先梯度清零

# Step 2。 Get our inputs ready for the network, that is,

# turn them into Tensors of word indices。

sentence_in

=

prepare_sequence

sentence

word_to_ix

#把句子中的每一個單詞都對應變成了一個序號,句子變成了一個序號張量(len()*1)

targets

=

torch

tensor

([

tag_to_ix

t

for

t

in

tags

],

dtype

=

torch

long

# Step 3。 Run our forward pass。

loss

=

model

neg_log_likelihood

sentence_in

targets

#訓練

# Step 4。 Compute the loss, gradients, and update the parameters by

# calling optimizer。step()

loss

backward

()

#計算梯度

optimizer

step

()

#更新引數

# Check predictions after training

with

torch

no_grad

():

precheck_sent

=

prepare_sequence

training_data

0

][

0

],

word_to_ix

print

model

precheck_sent

))

#打印出來的是最佳路徑的得分以及對應的tag序號

# We got it!

# 輸出:(tensor(20。4906), [0, 1, 1, 1, 2, 2, 2, 0, 1, 2, 2])

’‘’以下是對輸出形式的改變函式,與演算法無關‘’‘

# def get_entity(char_seq,tag_seq):

# length = len(char_seq)

# entity = []

# for i,(char,tag) in enumerate(zip(char_seq,tag_seq)):

# if tag == ’B‘:

# if ’ent‘ in locals()。keys():

# entity。append(ent)

# del ent

# ent = char

# if i+1 == length:

# entity。append(ent)

# if tag ==’I‘:

# ent = ent + “ ” + char

# if i+1 == length:

# entity。append(ent)

# if tag not in [’B‘,’I‘]:

# if ’ent‘ in locals()。keys():

# entity。append(ent)

# del ent

# continue

# return entity

# # Check predictions after training

# with torch。no_grad():

# precheck_sent = prepare_sequence(training_data[0][0], word_to_ix)

# # print(model(precheck_sent)) # 返回路徑最大分數和維特比演算法得出的路線

# path_score,state_path = model(precheck_sent)

# y_pred = [ix_to_tag[x] for x in state_path]

# entity_list = get_entity(training_data[0][0],y_pred)

# for x in entity_list:

# print(x)

# # We got it!

文章有問題的地方還請批評指正!大家一起討論學習呀 : )