問答機器人排序模型
- 1. 排序模型的介紹
- 2. 排序模型的實作思路
- 2.1 準備資料
- 2.1.1 兩個輸入
- 2.1.2 相似度準備
- 2.2 構建模型
- 2.3 模型的評估
- 3. 代碼實作
- 3.1 資料準備
- 3.1.1 對文本進行分詞分開存盤
- 3.1.2 準備word Sequence代碼
- 3.1.3 準備Dataset和DataLoader
- 3.2 模型的搭建
- 3.2.1 編碼部分
- atttention的計算
- Pooling實作
- 3.2.2 相似度計算部分
- 3.2.3 損失函式部分
- 使用DNN+均方誤差來計算得到結果
- 使用對比損失來計算得到結果
- 3.3 不同模型的結果對比
1. 排序模型的介紹
前面的課程中為了完成一個問答機器人,我們先進行了召回,相當于是通過海選的方法找到呢大致相似的問題,
通過現在的排序模型,我們需要精選出最相似的哪一個問題,回傳對應的答案
2. 排序模型的實作思路
我們需要實作的排序模型是兩個輸入,即兩個問題,輸出的是一個相似度,所以和之前的深度學習模型一樣,我們需要實作的步驟如下:
- 準備資料
- 構建模型
- 模型評估
- 對外提供介面回傳結果
2.1 準備資料
這里的資料,我們使用之前采集的百度問答的相似問題和手動構造的資料,那么,我們需要把他格式化為最終模型需要的格式,即兩個輸入和輸出的相似度,
2.1.1 兩個輸入
這里的輸入,我們可以使用單個字作為特征,也可以使用一個分詞之后的詞語作為特征,所以在實作準備輸入資料方法的程序中,可以提前準備,
2.1.2 相似度準備
這里我們使用每個問題搜索結果的前兩頁認為他們是相似的,相似度為1,最后兩頁的結果是不相似的,相似度為0,
2.2 構建模型
介紹模型的構建之前,我們先介紹下孿生神經網路(Siamese Network)和其名字的由來,
Siamese和Chinese有點像,Siamese是古時候泰國的稱呼,中文譯作暹羅,Siamese在英語中是“孿生”、“連體”的意思,為什么孿生和泰國有關系呢?
十九世紀泰國出生了一對連體嬰兒,當時的醫學技術無法使兩人分離出來,于是兩人頑強地生活了一生,1829年被英國商人發現,進入馬戲團,在全世界各地表演,1839年他們訪問美國北卡羅萊那州后來成為馬戲團的臺柱,最后成為美國公民,1843年4月13日跟英國一對姐妹結婚,恩生了10個小孩,昌生了12個,姐妹吵架時,兄弟就要輪流到每個老婆家住三天,1874年恩因肺病去世,另一位不久也去世,兩人均于63歲離開人間,兩人的肝至今仍保存在費城的馬特博物館內,從此之后“暹羅雙胞胎”(Siamese twins)就成了連體人的代名詞,也因為這對雙胞胎讓全世界都重視到這項特殊疾病,
所以孿生神經網路就是有兩個共享權值的網路的組成,或者只用實作一個,另一個直接呼叫,有兩個輸入,一個輸出,1993年就已經被用來進行支票簽名的驗證,
孿生神經網路通過兩個輸入,被DNN進行編碼,得到向量的表示之后,根據實際的用途來制定損失函式,比如我們需要計算相似度的時候,可以使用余弦相似度,或者使用 e x p ? ∣ ∣ h l e f t ? h r i g h t ∣ ∣ exp^{-||h^{left}-h^{right}||} exp?∣∣hleft?hright∣∣來確定向量的距離,
孿生神經網路被用于有多個輸入和一個輸出的場景,比如手寫字體識別、文本相似度檢驗、人臉識別等
在計算相似度之前,我們可以考慮在傳統的孿生神經網路的基礎上,在計算相似度之前,把我們的編碼之后的向量通過多層神經網路進行非線性的變化,結果往往會更加好,那么此時其網路結構大致如下:

其中Network1和network2為權重引數共享的兩個形狀相同的網路,用來對輸入的資料進行編碼,包括(word-embedding,GRU,biGRU等),Network3部分是一個深層的神經網路,包含(batchnorm、dropout、relu、Linear等層)
2.3 模型的評估
撰寫預測和評估的代碼,預測的程序只需要修改獲得結果,不需要上圖中的損失計算的程序
3. 代碼實作
3.1 資料準備
3.1.1 對文本進行分詞分開存盤
這里的分詞可以對之前的分詞方法進行修改
def cut_sentence_by_word(sentence):
# 對中文按照字進行處理,對英文不分為字母
letters = string.ascii_lowercase + "+" + "/" # c++,ui/ue
result = []
temp = ""
for word in line:
if word.lower() in letters:
temp += word.lower()
else:
if temp != "":
result.append(temp)
temp = ""
result.append(word)
if temp != "":
result.append(temp)
return result
def jieba_cut(sentence,by_word=False,with_sg=False,use_stopwords=False):
if by_word:
return cut_sentence_by_word(sentence)
ret = psg.lcut(sentence)
if use_stopwords:
ret = [(i.word, i.flag) for i in ret if i.word not in stopwords_list]
if not with_sg:
ret = [i[0] for i in ret]
return ret
3.1.2 準備word Sequence代碼
該處的代碼和seq2seq中的代碼相同,直接使用
3.1.3 準備Dataset和DataLoader
和seq2seq中的代碼大致相同
3.2 模型的搭建
前面做好了準備作業之后,就需要開始進行模型的搭建,
雖然我們知道了整個結構的大致情況,但是我們還是不知道其中具體的細節,
2016年AAAI會議上,有一篇Siamese Recurrent Architectures for Learning Sentence Similarity的論文(地址:https://www.aaai.org/ocs/index.php/AAAI/AAAI16/paper/download/12195/12023),整個結構如下圖:

可以看到word 經過embedding之后進行LSTM的處理,然后經過exp來確定相似度,可以看到整個模型是非常簡單的,之后很多人在這個結構上增加了更多的層,比如加入attention、dropout、pooling等層,
那么這個時候,請思考下面幾個問題:
-
attention在這個網路結構中該如何實作
-
之前我們的attention是用在decoder中,讓decoder的hidden和encoder的output進行運算,得到attention的weight,再和decoder的output進行計算,作為下一次decoder的輸入
-
那么在當前我們可以把
句子A的output理解為句子B的encoder的output,那么我們就可以進行attention的計算了和這個非常相似的有一個attention的變種,叫做
self attention,前面所講的Attention是基于source端和target端的隱變數(hidden state)計算Attention的,得到的結果是源端的每個詞與目標端每個詞之間的依賴關系,Self Attention不同,它分別在source端和target端進行,僅與source input或者target input自身相關的Self Attention,捕捉source端或target端自身的詞與詞之間的依賴關系,
-
-
dropout用在什么地方
- dropout可以用在很多地方,比如embedding之后
- BiGRU結構中
- 或者是相似度計算之前
-
pooling是什么如何使用
- pooling叫做池化,是一種降采樣的技術,用來減少特征(feature)的數量,常用的方法有
max pooling或者是average pooling
- pooling叫做池化,是一種降采樣的技術,用來減少特征(feature)的數量,常用的方法有
3.2.1 編碼部分
def forward(self, *input):
sent1, sent2 = input[0], input[1]
#這里使用mask,在后面計算attention的時候,讓其忽略pad的位置
mask1, mask2 = sent1.eq(0), sent2.eq(0)
# embeds: batch_size * seq_len => batch_size * seq_len * batch_size
x1 = self.embeds(sent1)
x2 = self.embeds(sent2)
# batch_size * seq_len * dim => batch_size * seq_len * hidden_size
output1, _ = self.lstm1(x1)
output2, _ = self.lstm1(x2)
# 進行Attention的操作,同時進行形狀的對齊
# batch_size * seq_len * hidden_size
q1_align, q2_align = self.soft_attention_align(output1, output2, mask1, mask2)
# 拼接之后再傳入LSTM中進行處理
# batch_size * seq_len * (8 * hidden_size)
q1_combined = torch.cat([output1, q1_align, self.submul(output1, q1_align)], -1)
q2_combined = torch.cat([output2, q2_align, self.submul(output2, q2_align)], -1)
# batch_size * seq_len * (2 * hidden_size)
q1_compose, _ = self.lstm2(q1_combined)
q2_compose, _ = self.lstm2(q2_combined)
# 進行Aggregate操作,也就是進行pooling
# input: batch_size * seq_len * (2 * hidden_size)
# output: batch_size * (4 * hidden_size)
q1_rep = self.apply_pooling(q1_compose)
q2_rep = self.apply_pooling(q2_compose)
# Concate合并到一起,用來進行計算相似度
x = torch.cat([q1_rep, q2_rep], -1)
def submul(self,x1,x2):
mul = x1 * x2
sub = x1 - x2
return torch.cat([sub,mul],dim=-1)
atttention的計算
實作思路:
- 先獲取attention_weight
- 在使用attention_weight和encoder_output進行相乘
def soft_attention_align(self, x1, x2, mask1, mask2):
'''
x1: batch_size * seq_len_1 * hidden_size
x2: batch_size * seq_len_2 * hidden_size
mask1:x1中pad的位置為1,其他為0
mask2:x2中pad 的位置為1,其他為0
'''
# attention: batch_size * seq_len_1 * seq_len_2
attention_weight = torch.matmul(x1, x2.transpose(1, 2))
#mask1 : batch_size,seq_len1
mask1 = mask1.float().masked_fill_(mask1, float('-inf'))
#mask2 : batch_size,seq_len2
mask2 = mask2.float().masked_fill_(mask2, float('-inf'))
# weight: batch_size * seq_len_1 * seq_len_2
weight1 = F.softmax(attention_weight + mask2.unsqueeze(1), dim=-1)
#batch_size*seq_len_1*hidden_size
x1_align = torch.matmul(weight1, x2)
#同理,需要對attention_weight進行permute操作
weight2 = F.softmax(attention_weight.transpose(1, 2) + mask1.unsqueeze(1), dim=-1)
x2_align = torch.matmul(weight2, x1)
Pooling實作
池化的程序有一個視窗的概念在其中,所以max 或者是average指的是視窗中的值取最大值還是取平均估值,整個程序可以理解為拿著視窗在源資料上取值
視窗有視窗大小(kernel_size,視窗多大)和步長(stride,每次移動多少)兩個概念
-
>>> input = torch.tensor([[[1,2,3,4,5,6,7]]]) >>> F.avg_pool1d(input, kernel_size=3, stride=2) tensor([[[ 2., 4., 6.]]]) #[1,2,3] [3,4,5] [5,6,7]的平均估值

def apply_pooling(self, x):
# input: batch_size * seq_len * (2 * hidden_size)
#進行平均池化
p1 = F.avg_pool1d(x.transpose(1, 2), x.size(1)).squeeze(-1)
#進行最大池化
p2 = F.max_pool1d(x.transpose(1, 2), x.size(1)).squeeze(-1)
# output: batch_size * (4 * hidden_size)
return torch.cat([p1, p2], 1)
3.2.2 相似度計算部分
相似度的計算我們可以使用一個傳統的距離計算公式,或者是exp的方法來實作,但是其效果不一定好,所以這里我們使用一個深層的神經網路來實作,使用pytorch中的Sequential物件來實作非常簡單
self.fc = nn.Sequential(
nn.BatchNorm1d(self.hidden_size * 8),
nn.Linear(self.hidden_size * 8, self.linear_size),
nn.ELU(inplace=True),
nn.BatchNorm1d(self.linear_size),
nn.Dropout(self.dropout),
nn.Linear(self.linear_size, self.linear_size),
nn.ELU(inplace=True),
nn.BatchNorm1d(self.linear_size),
nn.Dropout(self.dropout),
nn.Linear(self.linear_size, 2),
nn.Softmax(dim=-1)
)
在上述程序中,我們使用了激活函式ELU,而沒有使用RELU,因為在有噪聲的資料中ELU的效果往往會更好,
E L U ( ? x ? ) = m a x ( 0 , x ) + m i n ( 0 , α ? ( e x p ( x ) ? 1 ) ) ELU(*x*)=max(0,x)+min(0,α?(exp(x)?1)) ELU(?x?)=max(0,x)+min(0,α?(exp(x)?1)),其中 α ? \alpha? α?在torch中默認值為1,
通過下圖可以看出他和RELU的區別,RELU在小于0的位置全部為0,但是ELU在小于零的位置是從0到-1的,可以理解為正常的資料匯總難免出現噪聲,小于0的值,而RELU會直接把他處理為0,認為其實正常值,但是ELU卻會保留他,所以ELU比RELU更有魯棒性

3.2.3 損失函式部分
在孿生神經網路中我們經常會使用對比損失(Contrastive Loss),作為損失函式,對比損失是Yann LeCun提出的用來判斷資料降維之后和源資料是否相似的問題,在這里我們用它來判斷兩個句子的表示是否相似,
對比損失的計算公式如下:
L
=
1
2
N
∑
n
=
1
N
(
y
d
2
+
(
1
?
y
)
m
a
x
(
m
a
r
g
i
n
?
d
,
0
)
2
)
L = \frac{1}{2N}\sum^N_{n=1}(yd^2 + (1-y)max(margin-d,0)^2)
L=2N1?n=1∑N?(yd2+(1?y)max(margin?d,0)2)
其中
d
=
∣
∣
a
n
?
b
n
∣
∣
2
d = ||a_n-b_n||_2
d=∣∣an??bn?∣∣2?,代表兩個兩本特征的歐氏距離,y表示是否匹配,y=1表示匹配,y=0表示不匹配,margin是一個閾值,比如margin=1,
上式可分為兩個部分,即:
- y = 1時,只剩下左邊, ∑ y d 2 \sum yd^2 ∑yd2,即相似的樣本,如果距離太大,則效果不好,損失變大
- y=0的時候,只剩下右邊部分,即樣本不相似的時候,如果距離小的話,效果反而不好,損失變大
下圖紅色是相似樣本的損失,藍色是不相似樣本的損失

但是前面我們已經計算出了相似度,所以在這里我們有兩個操作
- 使用前面的相似度的結果,把整個問題轉化為分類(相似,不相似)的問題,或者是轉化為回歸問題(相似度是多少)
- 不是用前面相似度的計算結果部分,只用編碼之后的結果,然后使用對比損失,最后在獲取距離的時候使用歐氏距離來計算器相似度
使用DNN+均方誤差來計算得到結果
def train(model,optimizer,loss_func,epoch):
model.tarin()
for batch_idx, (q,simq,q_len,simq_len,sim) in enumerate(train_loader):
optimizer.zero_grad()
output = model(q.to(config.device),simq.to(config.device))
loss = loss_func(output,sim.to(config.deivce))
loss.backward()
optimizer.step()
if batch_idx%100==0:
print("...")
torch.save(model.state_dict(), './DNN/data/model_paramters.pkl')
torch.save(optimizer.state_dict(),"./DNN/data/optimizer_paramters.pkl")
model = SiameseNetwork().cuda()
loss = torch.nn.MSELoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
for epoch in range(1,config.epoch+1):
train(model,optimizer,loss,epoch)
使用對比損失來計算得到結果
#contrastive_loss.py
import torch
import torch.nn
class ContrastiveLoss(torch.nn.Module):
"""
Contrastive loss function.
"""
def __init__(self, margin=1.0):
super(ContrastiveLoss, self).__init__()
self.margin = margin
def forward(self, x0, x1, y):
# 歐式距離
diff = x0 - x1
dist_sq = torch.sum(torch.pow(diff, 2), 1)
dist = torch.sqrt(dist_sq)
mdist = self.margin - dist
#clamp(input,min,max),和numpy中裁剪的效果相同
dist = torch.clamp(mdist, min=0.0)
loss = y * dist_sq + (1 - y) * torch.pow(dist, 2)
loss = torch.sum(loss) / 2.0 / x0.size()[0]
return loss
之后只需要把原來的損失函式改為當前的損失函式即可
3.3 不同模型的結果對比
未完待續…
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/292856.html
標籤:AI
