還在調API寫所謂的AI“女友”,嘮了嘮了,教你基於python咱們“new”一個(深度學習)

語言: CN / TW / HK

highlight: a11y-dark theme: channing-cyan


前言

誒,標題有點欠揍是吧。好吧承認有點標題黨了,拖更大王要更新了。什麼你是從這篇博文:快速構建一個簡單的對話+問答AI (上)過來的。好吧被你發現了,我是把中間那一段拆開來了。好吧,之所以這樣做其實還是因為那篇文章是在是太長了,沒寫清楚,同時每一個模組都是獨立的,因此的話咱們專門拆開來再說下是咋乾的。咱們這是一篇獨立的博文,那麼為啥要獨立捏,因為我知道你可能並不需要一個比較完整的內容,如果你關注的是如何實現一個對話AI的話,那麼來這裡就對了。我們將單獨從資料集開始再講起。並且將真正完整的程式碼直接在咱們的博文當中貼出了。(因為第三個模組還在做,暫時沒有上傳倉庫)還是那句話,如果關注的是一個閒聊對話AI是如何實現的話,come here!!!

當然標題有點誇張,但是你要做一個所謂的對話AI女友是完全可以的,至少我們可以自己做服務了,當然呼叫服務,例如圖靈機器人啥的還是不錯的,少掉不少頭髮呢。

首先是目錄結構,你需要按照框起來的地方進行建立檔案。

在這裡插入圖片描述

這裡的話我就不去上次倉庫了,自己看著這個目錄建立,或者等我完整的專案上傳到GitHub後自己提取對應的檔案。

那麼同時的話,對應的資源比如語料資源在這: 連結:https://pan.baidu.com/s/1Bb0sWcITQLrkibDqIT8Qvg 提取碼:6666

裡面包含了語料和停用詞,分詞之類的

他們的格式是這樣的: 在這裡插入圖片描述

停用詞

開啟之後的話,格式大概是這樣的: 在這裡插入圖片描述

詞庫的格式也是類似的。

閒聊語料

這個閒聊也簡單,是這樣的: 在這裡插入圖片描述

E 是開始標誌 M 對話

這個到時候怎麼用,咱們在後面再說。

ok,我們這邊都建立好了資源都準備好了。那麼現在我們把資源放到對應的位置: 在這裡插入圖片描述

基礎知識

詞的表示

這部分的話我們先前說過,但是呢,這個是獨立的博文,因此咱們重複一下。

表達

計算機和我們人類是不一樣的,他只能進行基本的數字運算,在咱們先前的影象處理當中,影象的表達依然還是通過數值矩陣的,但是一個句子或者單純是如何表示的呢。所以為了能夠讓計算機可以處理到咱們的文字資料,咱們需要對文字做一點點處理。

那麼在這裡是如何做的呢,其實很簡單,既然計算機只能處理數字,對數字進行運算,那麼我們只需要把我們的一個句子轉化為一種向量就好了。那麼這個是如何做的呢?

其實非常簡單。

看下面一組圖就明白了:

在這裡插入圖片描述

我們通過一個詞典其實就可以完成一個向量的對映。

看到了吧,我們這個時候我們只需要對一個句子進行分詞,之後將每一個詞進行標號,這樣一來就可以實現把一個句子轉化為一個向量。

one-hot編碼

此時我們得到了一組序列,但是這個序列的表達能力是在是太弱了,只能表示出一個標號,不能表示出其他的特點。或者說,只有一個數字表示一個詞語實在是太單調了,1個詞語也應該由一個序列組成。那麼這個時候one-hot編碼就出來了。他是這樣做的: 在這裡插入圖片描述 首先一個詞,一個字,我們叫做token,那麼編碼的很簡單。其實就是這樣: 在這裡插入圖片描述 但是這樣是有問題的,那就是說,我們雖然實現了一個詞到向量的表示。但是這個表示方法顯然是太大了,假設有10000個詞語,那麼按照這種方式進行標號的話,那麼1個詞就是10000個維度。這樣顯然是不行的。所以這塊需要優化一下。

詞嵌入

這個原來解釋起來稍微複雜一點。你只需要需要知道他們的本質其實就是這樣的: 詞 ——> 向量空間1 ——> 向量空間2 現在向量空間1不合適,所以我們要想辦法能不能往空間2進行靠攏。

於是乎這裡大概就有了兩個方案:

1)嘗試將詞向量對映到一個更低維的空間; 2)同時保持詞向量在該低維空間中具備語義相似性,如此,越相關的詞,它們的向量在這個低維空間裡就能靠得越近。

對於第一個,咱們可以參考原來咱們做協同過濾推薦dome的時候,使用SVD矩陣分解來做。(關於這篇博文的話也是有優化的,優化方案將在本篇博文中檢視到,先插個眼)

那麼缺點的話也很明顯嘛,用咱們的這個方案:

1)親和矩陣的維度可能經常變,因為總有新的單詞加進來,每加進來一次就要重新做SVD分解,因此這個方法不太通用; 2)親和矩陣可能很稀疏,因為很多單詞並不會成對出現。

大致原理

ok,回到咱們的這個(這部分可以選擇跳過,知道這個玩意最後得到的是啥就好了),這個該怎麼做,首先的話,實現這個東西,大概是有兩種方案去做:Continuous Bag Of Words (CBOW)方法和n-gram方法。第一個方案的話,這個比較複雜,咱們這裡就不介紹了。

咱們來說說第二個方案。

首先咱們來說說啥是N-gram,首先原理的話也是比較複雜的,具體參考這個:https://blog.csdn.net/songbinxu/article/details/80209197

那麼我們這邊就是簡單說一下這個在咱們這邊N-gram實際是咋用的。

python [cuted[i:i+2]for i in range(len(cuted))]

其實就是這個,用程式碼表示,cuted是一個分好詞的句子。i+2表示跨越幾個。

這樣做的好處是,通過N-gram可以考慮到詞語之間的一個關係,如果我們使用這個方案來實現一個詞向量的話,那麼我們必然是可以能夠實現:“同時保持詞向量在該低維空間中具備語義相似性,如此,越相關的詞,它們的向量在這個低維空間裡就能靠得越近。”的。因為確實考慮到了之間的一個關係,那麼現在我們已經知道了大概N-garm是怎麼樣的了,其實就是一種方式,將一個句子相近的詞語進行連線,或者說是對句子進行一個切割,上面那個只是一種方式只有,這個我們在後面還會有說明,總之它是非常好用的一種方式。

ok,知道了這個我們再來介紹幾個名詞:

1.跳詞模型
跳詞模型,它是通過文字中某個單詞來推測前後幾個單詞。例如,根據‘rabbit’來推斷前後的單詞可能為‘a’,'is','eating','carrot'。在訓練模型時我們在文字中選取若干連續的固定長度的單詞序列,把前後的一些單詞作為輸出,中間的某個位置的單詞作為輸入。

2.連續詞袋模型
連續詞袋模型與跳詞模型恰好相反,它是根據文字序列中周圍單詞來預測中心詞。在訓練模型時,把序列中周圍單詞作為輸入,中心詞作為輸出。

這個的話其實和我們的這個關係不大,因為N-gram其實是句子-->詞 的一種方式,但是對我訓練的時候的輸入還是有幫助的,因為這樣輸入的話,我們是可以得到詞在句子當中的一種關聯關係的。

而embedding是詞到one-hot然後one-hot到低緯向量的變化過程。

實現

ok,扯了那麼多,那麼接下來看看我們如何實現這個東西。

我們需要一個詞向量,同時我們有很多詞語,因此我們將得到一個矩陣,這個矩陣叫做embedding矩陣。

我們首先隨機初始化embeddings矩陣,構建一個簡單的網路。初始化weights和biases,計算隱藏層的輸出。然後計算輸出和target結果的交叉熵,之後使用優化器完成一次反向傳遞,更新可訓練的引數,包括embeddings變數。並且我們將詞之間的相似度可以看作概率。

ok,我們直接看到程式碼,那麼咱們也是有兩個版本的。簡單版,複雜版。

簡單版

簡單版本的話,在pytorch當中有實現:

python embed=nn.Embedding(word_num,embedding_dim)

複雜版

那麼我們顯然是不滿足這個的,那麼我們還有複雜版本。就是自己動手,豐衣足食! 首先我們定義這個:

python class embedding(nn.Module): def __init__(self,in_dim,embed_dim): super().__init__() self.embed=nn.Sequential(nn.Linear(in_dim,200), nn.ReLU(), nn.Linear(200,embed_dim), nn.Sigmoid()) def forward(self,input): b,c,_=input.shape output=[] for i in range(c): out=self.embed(input[:,i]) output.append(out.detach().numpy()) return torch.tensor(np.array(output),dtype=torch.float32).permute(1,0,2)

很簡單的一個結構。 那麼我們輸入是上面,首先其實是我們one-hot編碼的一個矩陣。 我們其實流程就是這樣的:詞--->one-hot--->embedding/svd

ok,那麼我們的N-gram如何表示呢,其實這個更多的還是在於對句子的分解上,輸入的句子的詞向量如何表示的。

如何訓練

如何訓練的話,首先還是要在one-hot處理的時候再加一個處理,這個過程可能比較繞。就是說我們按照上面提到的詞袋模型進行構造我們的資料,我們舉個例子吧。

現在有這樣的一個文字,分詞之後,詞的個數是content_size。有num_word個詞。

```python import torch import re import numpy as np

txt=[] #文字資料 with open('peter_rabbit.txt',encoding='utf-8') as f: for line in f.readlines(): l=line.strip() spilted_sentence=re.split(" |;|-|,|!|\'",l) for w in spilted_sentence: if w !='': txt.append(w.lower()) vol=list(set(txt)) #單詞表 n=len(vol) #單詞表單詞數 vol_dict=dict(zip(vol,np.arange(n))) #單詞索引 ''' 這裡使用詞袋模型 每次從文字中選取序列長度為9,輸入單詞數為,8,輸出單詞數為1, 中心詞位於序列中間位置。並且採用pytorch中的emdedding和自己設計embedding兩種方法 詞嵌入維度為100。 ''' data=[] label=[]

for i in range(content_size): in_words=txt[i:i+4] in_words.extend(txt[i+6:i+10]) out_word=txt[i+5] in_one_hot=np.zeros((8,n)) out_one_hot=np.zeros((1,n)) out_one_hot[0,vol_dict[out_word]]=1 for j in range(8): in_one_hot[j,vol_dict[in_words[j]]]=1 data.append(in_one_hot) label.append(out_one_hot)

class dataset: def init(self): self.n=ci=config.content_size def len(self): return self.n def getitem(self, item): traindata=torch.tensor(np.array(data),dtype=torch.float32) trainlabel=torch.tensor(np.array(label),dtype=torch.float32) return traindata[item],trainlabel[item] ```

我們只是在投喂資料的時候按照詞袋模型進行投喂,或者連續模型也可以。

當然我們這裡所說的都只是說預訓練出一個模型出來,實際上,我們直接使用這個結構,然後進行正常的訓練完成我們的一個模型也是可以的。她是很靈活的,不是固定的!

那麼繼續預訓練的話就是按照詞袋模型來就好了(看不懂沒關係,跳過就好了)

```python import torch from torch import nn from torch.utils.data import DataLoader from dataset import dataset import numpy as np

class model(nn.Module): def init(self): super().init() self.embed=embedding(num_word,100) self.fc1=nn.Linear(num_word,1000) self.act1=nn.ReLU() self.fc2=nn.Linear(1000,num_word) self.act2=nn.Sigmoid() def forward(self,input): b,,=input.shape out=self.embed (input).view(b,-1) out=self.fc1 (out) out=self.act1(out) out=self.fc2(out) out=self.act2(out) out=out.view(b,1,-1) return out if name=='main': pre_model=model() optim=torch.optim.Adam(params=pre_model.parameters()) Loss=nn.MSELoss() traindata=DataLoader(dataset(),batch_size=5,shuffle=True) for i in range(100): print('the {} epoch'.format(i)) for d in traindata: p=model(d[0]) loss=Loss(p,d[1]) optim.zero_grad() loss.backward() optim.step() ```

這樣一來就可以初步完成預訓練,你只需要載入好embeding部分的權重就好了,這個只是加快收斂的一種方式。

轉換後的形狀

最終,詞嵌入的話,得到的矩陣是將one-hot變化為了這樣的矩陣 在這裡插入圖片描述

ok,詞的表達已經🆗了,那麼接下來我們在簡單介紹一下RNN。 (當然對於這一部分,實際上的話其實還有別的方法,但是咱們這邊只是用到這些東西,所以只是介紹這個)

RNN迴圈網路

RNN

這個RNN的話,咋說呢,其實挺簡單的,但是有幾個點可能是比較容易誤導人的,搞清楚這個結構的話,對於我們後面對於LSTM,GRU這種網路的架構可能會更好了解,其實包括LSTM,GRU的話其實本質上還是挺簡單的。當然能夠直接提出這個東西的人是非常厲害的,不過不管怎麼說他們都是屬於迴圈神經網路的一個大家族的,只是在資料處理上面多了一點點東西。那麼理解了RNN之後的話,對於我後面理解LSTM,GRU裡面它的一個數據的變幻,傳遞,原理。因為後面的話,我們還是要手寫實現這個GRU的(LSTM也是一樣的,但是GRU少了點引數,消耗的計算資源少一點點)。所以對於這一部分還是有必要好好嘮一嘮的。

首先我們來看到基本的神經網路:

在這裡插入圖片描述

這是一個簡單的前饋神經網路,也是我們最常見的神經網路。

接下來是我們的RNN神經網路,在大多數情況下,我們經常會提到這幾個名詞:時間步,最後一層輸出等等。

那麼在這裡的話,我們需要理解展開的其實只有一個東西,那就是對應時間步的理解,什麼是上一層網路的輸出,他們之間的引數是如何傳遞的。

RNN投影圖

那麼在此之前,我們先來看看RNN的網路結構大概是什麼樣子的。 大多數情況下,你搜索到的圖片可能是這樣的: 在這裡插入圖片描述

首先承認這張圖非常的簡潔,以至於你可能一開始沒有反應過來,什麼體現迴圈,體現時間步的地方在哪。其實這裡的話,這種圖其實只是一個縮略平面圖。

RNN是三維立體的

但是實際上,如果需要用畫圖來表示的話,RNN其實是立體的一個樣子。大概長這個樣子: 在這裡插入圖片描述 可能有點抽象,但是它的意思其實就是這樣的,這個其實是RNN真正的樣子,之後通過對不同的時間步的輸出進行不同的處理,最終我們還可以將RNN進行分類。

OK,這個就是我們在RNN裡面需要注意的點,它的真實結構是這樣的,是一個三維度的結構。同樣的接下來要提到的LSTM,GRU都是。

OK,接下來還沒完,我們現在需要不目光放長遠一點,首先是在RNN裡面對於層的概念,我們接下來會說什麼什麼層,搭建幾層的一個LSTM,GRU之類的,或者說幾層的RNN,這個層其實是指,一個時間步上有幾個立體的層,而不是說先前平面的那種網路,說幾層幾層。因為實際上,咱們這裡圖畫的就一層全連線(輸入層不算),但是在時間步上,它是N層,你有幾個X就有幾個層。

我們拿一個句子為例,假設一句話有5個單詞,或者說處理之後有5個詞語。那麼RNN就是把每一個詞的詞向量作為輸入,按照順序,按照上面圖的順序進行輸入。此時需要做的就是迴圈5次。

LSTM&GRU

那麼之後的話,咱們再來說說LSTM和GRU,他們呢叫做長短期記憶網路,其實就是最low的RNN的一個升級版,對資訊進一步處理。我們對於模型的調優,優化說白了,除了效能的優化,就是對資訊的最大利用(增加資訊,或者對重點資訊進行提取)。所以基本上為什麼大模型的效果很好,其實不考慮對資訊的利用率,單單是對資訊的使用就已經達到了超大的規模,這效果肯定是比小模型好一點的。

那麼這裡的話,我們就簡單過一下這個結構圖吧。

首先是LSTM,其實的話他這裡主要是引入了一個東西,叫做記憶。 在這裡插入圖片描述

c就是記憶,因為剛剛的RNN,的話其實更像是一個一階的馬爾科夫,那麼匯入這個的話,就相當於日記,你不僅僅知道了昨天做了什麼,還知道了前天做了什麼,這樣的話對於資訊的利用坑定是上去了的。那麼這個是它的一個單元。 巨集觀上還是這樣的: 在這裡插入圖片描述

同理GRU也是一樣的 在這裡插入圖片描述 但是這裡的話少了一個c 其實還是說把Ht和c合在了一起,他們效果是差不多的,各有各的好處,你用LSTM還能多得到一個日記本,用GRU的話其實相當於,你把日記寫在了腦子裡面。好處是省錢,壞處是有時候要你女朋友可能需要檢查日記(雖然我知道你有95%以上的概率是沒有的,一般設定0.05 作為閾值,低於這個概率,基本上我們認為G了)

ok,這些我們都說完了

構建資料

nice到這裡了,這部分的話,我們還需要知道一些東西。我們現在知道了圖的表達,也知道了RNN大概是啥,啥是時間步之類的。這裡重點對應RNN就是那玩意是3維的,包括那個LSTM,GRU其實都是。

那麼現在還需要幹啥呢,當然是第一部分,我們要把詞變成序列呀。

配置

在開始之前,我們還需要給出配置哈,那麼我們這裡先給出來,在這裡: 在這裡插入圖片描述

```python """ just configuration for this project to run """

import pickle

auto_fix = True

jieba_config = { "word_dict":"./../../data/word/word40W.txt", "stop_dict":"./../../data/word/stopWordBaiDu.txt", }

data_path = { "xiaohuangji": "./../../data/XiaoHuangJi50W.conv", "QA": "./../../data/QA5W.json" }

""" Encoder and Decoder using same config params in here """ chatboot_config = {

"target_path_no_by_word":"./../../data/chat/target_no_by_word.txt",
"input_path_no_by_word": "./../../data/chat/input_no_by_word.txt",
"word_corpus_no_by_word_input":"./../../data/chat/word_corpus_input_no_by_word.pkl",
"word_corpus_no_by_word_target":"./../../data/chat/word_corpus_target_no_by_word.pkl",

"target_path_by_word": "./../../data/chat/target_by_word.txt",
"input_path_by_word": "./../../data/chat/input_by_word.txt",
"word_corpus_by_word_input": "./../../data/chat/word_corpus_input_by_word.pkl",
"word_corpus_by_word_target": "./../../data/chat/word_corpus_target_by_word.pkl",

"seq2seq_model_no_by_word":"./../../data/chat/seq2seq_model_no_by_word.pth",
"optimizer_model_no_by_word":"./../../data/chat/optimizer_model_no_by_word.pth",

"seq2seq_model_by_word": "./../../data/chat/seq2seq_model_by_word.pth",
"optimizer_model_by_word": "./../../data/chat/optimizer_model_by_word.pth",

"batch_size": 128,
"collate_fn_is_by_word": False,

"input_max_len":12,
"target_max_len": 12,
"out_seq_len": 15,
"dropout": 0.3,
"embedding_dim": 300,
"padding_idx": 0,
"sos_idx": 2,
"eos_idx": 3,
"unk_idx": 1,
"num_layers": 2,
"hidden_size": 128,
"bidirectional":True,
"batch_first":True,
# support 0,1,..3(gpu) and cup
"drive":"0",
"num_workers":0,
"teacher_forcing_ratio": 0.1,
# just support "dot","general","concat"
"attention_method":"general",
"use_attention": True,
"beam_width": 3,
"max_norm": 1,
"beam_search": True

}

def chat_load_(path,by_word,is_target,fixed=True, min_count=5): from corpus.chatbot_corpus.build_chat_corpus import Chat_corpus, compute_build after_fix = False ws = None try: ws = pickle.load(open(path, 'rb')) except: if (auto_fix): print("fixing...") chat_corpus = Chat_corpus() compute_build(chat_corpus=chat_corpus, fixed=fixed, min_count=min_count, by_word=by_word, is_target=is_target) after_fix = True

if (after_fix):
    ws = pickle.load(open(path, 'rb'))
return ws

def word_corpus_no_by_word_input_load(): path = chatboot_config.get("word_corpus_no_by_word_input") return chat_load_(path,is_target=False,by_word=False)

def word_corpus_no_by_word_target_load(): path = chatboot_config.get("word_corpus_no_by_word_target") return chat_load_(path,is_target=True,by_word=False)

def word_corpus_by_word_input_load(): path = chatboot_config.get("word_corpus_by_word_input") return chat_load_(path,is_target=False,by_word=True)

def word_corpus_by_word_target_load(): path = chatboot_config.get("word_corpus_by_word_target") return chat_load_(path,is_target=True,by_word=True)

chatboot_config_load = { "word_corpus_no_by_word_input_load": word_corpus_no_by_word_input_load(), "word_corpus_no_by_word_target_load": word_corpus_no_by_word_target_load(), "word_corpus_by_word_input_load": word_corpus_by_word_input_load(), "word_corpus_by_word_target_load": word_corpus_by_word_target_load(), }

```

這個的話,是我們對話AI需要的配置檔案。

資料集準備

ok,我們開始準備資料集了。這裡注意咯,如果你想要訓練出一個AI女友的話,這部分很關鍵喲~。首先資料是這個樣子的。 在這裡插入圖片描述

那麼我們要做的是啥呢,首先我們這裡把第一句話作為我們的輸入,也就是說我們假設,第一句話是你要說的話。第二句話是你期望AI輸出的話,那麼我們把第一句話作為input,第二句作為target。我們期望的是,你輸入一個input,AI能夠輸出類似與target的話來。

那麼我們先要做的就是對資料的切分。 這個就是我們切分之後的結果: 在這裡插入圖片描述 我們這裡的話還可以實現按照一個一個字來分和按照jieba進行分詞的效果。也就是說如果你覺得按照jieba分詞的效果不好,你可以試著直接按照字去分詞。

分詞

歐克,那麼我們現在要做的先是實現我們的分詞。 這個的話把程式碼放在這裡: 在這裡插入圖片描述

實現是這個:

```python """ this model just for cutting words """

import jieba import jieba.posseg as pseg from tqdm import tqdm, trange from config.config import jieba_config import string

jieba.load_userdict(jieba_config.get("word_dict")) jieba = jieba pseg = pseg string = string with open(file=jieba_config.get("stop_dict"),encoding='utf-8') as f: lines = tqdm(f.readlines(),desc="loading stop word") StopWords = {}.fromkeys([line.rstrip() for line in lines ])

print("\033[0;32;40m all loading is finished!\033[0m")

class Cut(object):

def __init__(self,other_letters=None):
    self.letters = string.ascii_letters
    self.stopword = StopWords

def __stop_not_sign(self,result):
    result_rel = []
    for res in result:
        if (res not in self.stopword):
            result_rel.append(res)
    return result_rel

def __stop_with_sign(self, result):
    result_rel = []
    for res in result:
        if (res.word not in self.stopword):
            result_rel.append((res.word,res.flag))
    return result_rel

def cut(self,sentence,by_word=False,
          use_stop_word=False,with_sg=False
          ):
    """
    :param sentence:
    :param by_word:
    :param use_stop_word:
    :param with_sg:
    :return:
    """
    if(by_word):
        return self.cut_sentence_by_word(sentence)
    else:
        '''
        without by word,so there will be cutting by jieba
        '''
        if (with_sg):
            result = pseg.lcut(sentence)
            if(use_stop_word):
                result = self.__stop_with_sign(result)
        else:
            result = jieba.lcut(sentence)
            if (use_stop_word):
                result = self.__stop_not_sign(result)
        return result

def cut_sentence_by_word(self,sentence):
    """
    it can cut English sentences and Chinese
    :param sentence:
    :return:
    """
    result = []
    temp = ""
    for word in sentence:
        if word.lower() in self.letters:
            temp+=word
        else:
            if(temp!=""):
                result.append(temp)
                temp = ""
            else:
                result.append(word.strip())
    if(temp!=""):
        result.append(temp.lower())
    return result

```

劃分

之後的話,我們就可以劃分了。這裡的話我們要做的不僅僅是劃分,我們還需要構建一個方法,去能夠把一個句子轉換為一個序列。那麼這個實現的話也是在這裡一起實現的。

這部分的程式碼其實也很簡單,沒啥好說的,我們直接看到程式碼。 在這裡插入圖片描述

```python """ for building corpus for chatboot running This will be deployed in a white-hole, possibly in version 0.7 """ import pickle from tqdm import tqdm from config import config from utils.cut_word import Cut

class Chat_corpus(object):

def __init__(self):
    self.Cut = Cut()
    self.PAD = 'PAD'
    self.UNKNOW = 'UNKNOW'
    self.EOS = 'EOS'
    self.SOS = 'SOS'
    self.word2index={
        self.PAD: config.chatboot_config.get("padding_idx"),
        self.SOS: config.chatboot_config.get("sos_idx"),
        self.EOS: config.chatboot_config.get("eos_idx"),
        self.UNKNOW: config.chatboot_config.get("unk_idx"),
    }
    self.index2word = {}
    self.count = {}

def fit(self,sentence_list):
    """
    just for counting word
    :param sentence_list:
    :return:
    """
    for word in sentence_list:
        self.count[word] = self.count.get(word,0)+1

def build_vocab_chat(self,min_count=None,max_count=None,max_feature=None):
    """
    build word dict,this need to save by pickle in computer memory
    :return:
    """

    temp = self.count.copy()
    for key in temp:
        cur_count = self.count.get(key,0)
        if(min_count !=None):
            if(cur_count<min_count):
                del self.count[key]

        if(max_count!=None):
            if(cur_count>max_count):
                del self.count[key]

    if(max_feature!=None):
        self.count = dict(sorted(self.count.items(),key= lambda x:x[1],
                                  reverse=True
                                  )[:max_feature]
                           )

    for key in self.count:
        self.word2index[key] = len(self.word2index)
    self.index2word = {item[1]:item[0] for item in self.word2index.items()}

def transform(self,sentence,max_len,add_eos=False):
    if(len(sentence)>max_len):
        sentence = sentence[:max_len]
    sentence_len = len(sentence)
    if(add_eos):
        sentence = sentence+[self.EOS]
    if(sentence_len<max_len):
        sentence = sentence +[self.PAD]*(max_len-sentence_len)
    result = [self.word2index.get(i,self.word2index.get(self.UNKNOW)) for i in sentence]
    return result

def inverse_transform(self,indices):
    """
    index ---> sentence
    :param indices:
    :return:
    """
    result = []
    for i in indices:
        if(i==self.word2index.get(self.EOS)):
            break
        result.append(self.index2word.get(i,self.UNKNOW))
    return result

def __len__(self):
    return len(self.word2index)

def __by_word(self,data_lines):
    for line in data_lines:
        for word in self.Cut.cut(line,by_word=True):
            self.word2index[word] = self.word2index.get(word,0)+1

def __by_not_word(self,data_lines):
    for line in  data_lines:
        for word in self.Cut.cut(line,by_word=False):
            self.word2index[word] = self.word2index.get(word, 0) + 1

def division(self,by_word=False,use_stop_word=False):
    """
    this funcation just for dividing input and target in xiaohuangji corpus
    :return:
    """
    count_input = 0
    count_target = 0
    temp_sentence = []

    if(by_word):
        middle_prx = ""
    else:
        middle_prx = "_no"

    target_save = open(config.chatboot_config.get("target_path"+middle_prx+"_by_word"),'a',encoding='utf-8')
    input_save  = open(config.chatboot_config.get("input_path"+middle_prx+"_by_word"),'a',encoding='utf-8')
    xiaohuangji_path = config.data_path.get("xiaohuangji")

    with open(xiaohuangji_path,'r',encoding='utf-8') as file:
        file_lines = tqdm(file.readlines(),desc="division xiaohuangji")
        for line in file_lines:
            line = line.strip()
            if (line.startswith("E")):
                continue
            elif (line.startswith("M")):
                line = line[1:].strip()
                line = self.Cut.cut(line, by_word, use_stop_word)
                temp_sentence.append(line)

            if(len(temp_sentence)==2):
                """
                Because the special symbol has a certain possibility, 
                it is used as the input of the user.
                Therefore, retain that special kind of "symbolic dialogue" corpus
                """
                if(len(line)==0):
                    temp_sentence = []
                    continue
                input_save.write(" ".join(line)+'\n')
                count_input+=1
                target_save.write(" ".join(line)+'\n')
                count_target+=1
                temp_sentence=[]
        input_save.close()
        target_save.close()
        assert count_target==count_input,'count_target need equal count_input'
        print("\033[0;32;40m process is finished!\033[0m")
        print("The input len is:",count_input,"\nThe target len is:",count_target)

def compute_build(chat_corpus,fixed=False, by_word=False,min_count=5, max_count=None,max_feature=None, is_target=True, ): """ for computing fit function with input and target file :param fixed: if True when error coming will try to fix by itself :return: """

if (by_word):
    middle_prx = ""
else:
    middle_prx = "_no"


after_fixed = False
lines = []

try:
    if(is_target):
        lines = open(config.chatboot_config.get("target_path"+middle_prx+"_by_word"), 'r', encoding='utf-8').readlines()
    else:
        lines = open(config.chatboot_config.get("input_path"+middle_prx+"_by_word"), 'r', encoding='utf-8').readlines()
except Exception as e:
    if(fixed):
        chat_corpus.division(by_word=by_word)
        after_fixed = True
    else:
        raise Exception("you need use Chat_corpus division function first! ")

if(after_fixed):
    if (is_target):
        lines = open(config.chatboot_config.get("target_path" + middle_prx + "_by_word"), 'r',
                     encoding='utf-8').readlines()
    else:
        lines = open(config.chatboot_config.get("input_path" + middle_prx + "_by_word"), 'r',
                     encoding='utf-8').readlines()
data_lines = tqdm(lines,desc="building")
for line in data_lines:
    chat_corpus.fit(line.strip().split())

chat_corpus.build_vocab_chat(min_count,max_count,max_feature)
if(is_target):

    pickle.dump(chat_corpus,open(config.chatboot_config.get("word_corpus"+middle_prx+"_by_word_target"),'wb'))
else:

    pickle.dump(chat_corpus, open(config.chatboot_config.get("word_corpus" + middle_prx + "_by_word_input"), 'wb'))

if name == 'main': chat_corpus = Chat_corpus() compute_build(chat_corpus,fixed=True,min_count=5,by_word=False,is_target=True)

```

那麼咱們這裡的話就是說,實現了這樣的方法:

  • 劃分input 和 target,劃分之後的效果是這個樣子的: 這裡的話有一個重點,那就是必須保證對話是成對出現的,也就是生成的檔案input和target對應的行數是一樣的,同時注意有沒有空行。 -在這裡插入圖片描述
  • 構造對映詞典,也就是達成這樣的目標 在這裡插入圖片描述
  • 儲存

資料集載入

之後的話就是去構建我們的資料集合了,那麼在這裡的話需要注意的就是重寫一個函式就好了。這個是我們使用pytorch必不可少的工作。 那麼實現的話就是這樣的:

在這裡插入圖片描述

```python """ dataSet about chat_boot """ from torch.utils.data import DataLoader,Dataset from boot.chatboot.encoder import Encoder from config import config import torch class Chat_dataset(Dataset):

r"""in there you will get this:

Prefix dict has been built successfully.
loading stop word: 100%|██████████| 1395/1395 [00:00<00:00, 1400443.77it/s]
 all loading is finished!
tensor([[   14,  6243,   925,  ...,   515,    66,  1233],
        [   20,    34,  2173,  ...,   710,     7,     9],
        [12422,    20,    42,  ...,     9,    14,   236],
        ...,
        [ 1636,     1,     1,  ...,     1,     1,     1],
        [  531,     1,     1,  ...,     1,     1,     1],
        [ 8045,     1,     1,  ...,     1,     1,     1]])
tensor([[  165, 19617,   118,  ...,     1,     1,     1],
        [  249,    15,    12,  ...,     1,     1,     1],
        [  153,     8,   153,  ...,     1,     1,     1],
        ...,
        [  329,    58,     3,  ...,     1,     1,     1],
        [  681,     0,  2625,  ...,     1,     1,     1],
        [ 5245,  3641,    15,  ...,     1,     1,     1]])
tensor([20, 19, 16, 15, 13, 13, 12, 12, 11, 11, 11, 11, 11,  9,  9,  9,  9,  8,
         8,  8,  8,  8,  8,  8,  7,  7,  7,  7,  7,  7,  6,  6,  6,  6,  6,  6,
         6,  6,  6,  6,  5,  5,  5,  5,  5,  5,  5,  5,  5,  5,  5,  5,  4,  4,
         4,  4,  4,  4,  4,  4,  4,  4,  4,  4,  4,  4,  4,  4,  4,  4,  4,  4,
         4,  4,  3,  3,  3,  3,  3,  3,  3,  3,  3,  3,  3,  3,  3,  3,  3,  3,
         3,  3,  3,  3,  3,  3,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2,
         2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  1,  1,  1,  1,  1,  1,  1,  1,
         1,  1])
tensor([ 7,  8,  3,  6,  2,  5,  2,  8,  5,  4,  2,  3,  3, 17,  3,  3, 10,  3,
         2, 71, 13,  1,  9, 10, 11, 10, 12,  3,  3,  8,  1, 10,  2, 11,  2,  9,
         2,  3,  8,  2,  3,  3,  4,  3,  3,  6, 40,  3,  8,  1, 30,  2,  7,  6,
         5, 74,  1,  9,  5,  5, 17,  4,  6,  5, 13,  2, 11,  3,  2,  6,  5,  2,
         2,  5,  3, 10,  5, 14,  3,  6,  2,  3, 18,  6,  9,  3,  4,  6,  3,  1,
         1,  7, 10,  6,  6,  3, 14,  2,  2,  7,  9,  6,  9,  3,  3,  9,  2,  3,
         7,  1,  1,  3,  4,  6,  6,  7,  1,  4,  6,  2,  6,  3,  5,  3,  2,  2,
         3,  6])

"""

def __init__(self,by_word=False):

    if (by_word):
        middle_prx = ""
    else:
        middle_prx = "_no"

    self.target_lines = open(config.chatboot_config.get("target_path" + middle_prx + "_by_word"), 'r',
                        encoding='utf-8').readlines()
    self.input_lines = open(config.chatboot_config.get("input_path" + middle_prx + "_by_word"), 'r',
                       encoding='utf-8').readlines()

    assert len(self.target_lines)==len(self.input_lines),"len need equal"

def __getitem__(self, index):
    input_data = self.input_lines[index].strip().split()
    target_data = self.target_lines[index].strip().split()
    if(len(input_data)==0):
        raise Exception("the input_data's length is: 0")
    input_length = len(input_data) if len(input_data)<config.chatboot_config.get("input_max_len") else config.chatboot_config.get("input_max_len")
    target_lenth = len(target_data) if len(target_data)<config.chatboot_config.get("target_max_len")+1 else config.chatboot_config.get("target_max_len")+1
    return input_data, target_data, input_length, target_lenth

def __len__(self):
    return len(self.input_lines)

def collate_fn(batch):

if(config.chatboot_config.get("collate_fn_is_by_word")):
    input_ws = config.chatboot_config_load.get("word_corpus_by_word_input_load")
    target_ws = config.chatboot_config_load.get("word_corpus_by_word_target_load")
else:
    input_ws = config.chatboot_config_load.get("word_corpus_no_by_word_input_load")
    target_ws = config.chatboot_config_load.get("word_corpus_no_by_word_target_load")

batch = sorted(batch,key=lambda x:x[-2],reverse=True)
input_data, target_data, input_length, target_lenth = zip(*batch)
input_data = [input_ws.transform(i, max_len=config.chatboot_config.get("input_max_len")) for i in input_data]
target_data = [target_ws.transform(i, max_len=config.chatboot_config.get("target_max_len"),add_eos=True) for i in target_data]

input_data = torch.LongTensor(input_data)
target_data = torch.LongTensor(target_data)
input_length = torch.LongTensor(input_length)
target_lenth = torch.LongTensor(target_lenth)

return input_data, target_data, input_length, target_lenth

if name == 'main':

chat_dataset = Chat_dataset()
train_data_loader = DataLoader(chat_dataset, batch_size=config.chatboot_config.get("batch_size"),
                               shuffle=True,
                               collate_fn=collate_fn)


for idx,(input_data, target_data, input_length, target_lenth) in enumerate(train_data_loader):
    print(input_data.max())
    print(target_data)
    print(input_length)
    print(target_lenth)
    break

```

那麼這個時候的話,我們的資料準備工作,做完這個之後,執行程式。沒有問題的話,在你的檔案目錄下面將會產生這些檔案。 在這裡插入圖片描述 沒有框起來的沒有,因為那個是訓練完之後才有的。

模型搭建

歐克,之後的話就來到了我們的模型搭建部分了。首先在這裡的話我們使用到的是非常經典的Seq2Seq(低情商:low) 我們先簡單瞭解一下這個玩意吧,這個東西看不太懂沒關係,因為沒辦法博文就這樣,包括論文也是隻能概括,有基礎很好理解,沒有隻能補一下。但是彆著急,在我這裡,你並不需要知道太多,瞭解即可。我只告訴你最本質的是啥,就是seq2seq它到底是啥,然後有哪些概念。之後大概的結構是啥。

基本概念

首先的話,這個玩意是這樣的結構大概: 在這裡插入圖片描述

他有一個編碼器和解碼器。那麼這個就是最重要的在這個網路當中。 同時他們詳細一點的關係是這樣的: 在這裡插入圖片描述

編碼器和解碼器都是一個RNN。

OK,知道了這個我們再來說說為啥是這樣的結構。首先我們為什麼需要使用到RNN,因為我們一個句子輸入進去的是一個詞向量。每一個詞之間相互關聯。上下詞之間存聯絡。因此我們提出了RNN,在基礎上我們又提出了LSTM,GRU這種RNN網路。目的就是解決這種像這種相互關聯的內容,之後的話我們把按照順序把每一個詞輸入進網路,一個接一個並且把上一個詞的輸出同時作為輸入。也就是所謂的時間步。因此我們首先使用者輸入了一個句子得到一個詞向量,那麼我們需要進行解析。因此我們首先在輸入的地方需要一個迴圈神經網路,之後我們需要得到一個句子作為輸出,同樣的這個也是一個接一個的,因為我們也需要一個詞一個詞前去生成,那麼這個是一個反過來的過程。所以此時我們需要第二個網路,並且這個網路也是一個迴圈網路,並且由於需要反過來,因此在實現上我們需要手寫RNN的迴圈。在我們這邊是使用GRU來做的,因此手寫GRU的一個迴圈。

所以你看到了我們需要兩個GRU,並且按照咱們剛剛說的,我們把這個玩意一個叫做編碼器,一個叫做解碼器。

之後我們理解了為啥要兩個網路,那麼同樣的我們如何得到句子呢?我們首先輸入了一個句子,之後由解碼器生成句子。這裡的話其實是這樣的,假設我們需要生成的句子最長是10個。我們的訓練集當中,或者說資料當中,有1W個詞。這個很正常的,畢竟我們訓練的資料都是幾十萬幾步的。那麼這個時候的話,我們其實要做的話就是直接預測一個概率,也就是說我們在解碼器最後面加入一個全連線層,之後通過softmax,這種函式,轉化為一個概率。10個詞的句子,1W個單詞。那麼我們最後一句話的話就會得到10*1W的概率矩陣,每一個句子的位置上,也就是時間步上,我們都預測概率。(當然你也可以學學YOLO直接把這個問題變成一個迴歸問題)。

所以假設你上面都沒有聽懂,那麼你只需要記住,那就是,我們輸入一個句子,通過seq2seq當中的編碼器和解碼器相互作用。最終生成了一個句子,在每一個位置上,在資料當中所有詞出現的概率,然後把概率最大的那個詞作為當前位置上要生成的詞,最後組成一句話就好了。那麼這個過程直到達到你預定的要生成的句子長度外,出現停止符號的時候也會停止。這個在我們的配置檔案當中寫了一個停止的標識。

Encoder搭建

OK,我們這邊來搭建一下編碼器,這個比較簡單。直接看就好了。 在這裡插入圖片描述

```python import torch.nn as nn from torch.nn.utils.rnn import pack_padded_sequence, pad_packed_sequence from config import config

class Encoder(nn.Module): def init(self,by_word=False): super(Encoder,self).init()

    if(by_word):
        self.input_ws = config.chatboot_config_load.get("word_corpus_by_word_input_load")
    else:
        self.input_ws = config.chatboot_config_load.get("word_corpus_no_by_word_input_load")

    self.embedding = nn.Embedding(
        num_embeddings=len(self.input_ws),
        embedding_dim=config.chatboot_config.get("embedding_dim"),
        padding_idx=config.chatboot_config.get("padding_idx")
    )

    self.gru = nn.GRU(input_size=config.chatboot_config.get("embedding_dim"),
                      dropout=config.chatboot_config.get("dropout"),
                      num_layers=config.chatboot_config.get("num_layers"),
                      hidden_size=config.chatboot_config.get("hidden_size"),
                      bidirectional=config.chatboot_config.get("bidirectional"),
                      batch_first=config.chatboot_config.get("batch_first")
                      )

def forward(self,input_data,input_length):
    embeded = self.embedding(input_data)
    embeded = pack_padded_sequence(embeded,input_length.cpu(),batch_first=True)

    out,hidden = self.gru(embeded)

    """
    in there return:
    hidden: num_layers*2,batch_size,hidden_size
    out: batch_size,sentence_len,hidden_size
    """
    out,_ = pad_packed_sequence(out,
                                     batch_first=config.chatboot_config.get("batch_first"),
                                     padding_value=config.chatboot_config.get("padding_idx")
                                     )
    return out,hidden

```

Decoder

之後是我們decoder的搭建。那麼在這塊的話,我們還簡答地加入了一個注意力機制:(感興趣的可以自己去看這篇論文,是2015年出來的:https://arxiv.org/pdf/1508.04025.pdf)這裡的話咱們就不介紹了)

同時的話,我們還對這個預測做了一個優化,剛剛是說我們在每一個位置上,找的都是概率最大的一個詞,然後作為這個位置的詞,直到達到了我們預定的長度,或者說這個位置概率最大的詞是結束標誌。然後停止,那麼在這裡的話就容易出現一個問題,那就是每一步最優不一定代表全域性最優,比如當前選了這個詞,概率是0.3,之後下一步選一個詞是0.2。而如果在上一步選擇0.25的概率的詞,下一步的一個詞的概率有0.6,那麼相對來說0.3和0.25差距可能不大,但是0.6和0.2差距是很大的。因此為了解決這個問題,有一個演算法叫做beamsearch。這個玩意就是說都會走一遍,最後選出看起來效果還不錯的序列作為輸出。

Attention機制

首先來看到我們的Attention 在這裡插入圖片描述

那麼程式碼是這裡:

```python """ The luong attention in there """ import torch.nn as nn import torch.nn.functional as F from config import config import torch class LuongAttention(nn.Module):

def __init__(self,method="general"):
    super(LuongAttention,self).__init__()
    assert method in ["dot","general","concat"],'method err just support "dot","general","concat"'
    self.method = method

    self.chatboot_encoder_hidden_size = config.chatboot_config.get("hidden_size")*2 if config.chatboot_config.get(
            "bidirectional") else config.chatboot_config.get("hidden_size")
    self.chatboot_decoder_hidden_size = config.chatboot_config.get("hidden_size")*2 if config.chatboot_config.get(
            "bidirectional") else config.chatboot_config.get("hidden_size")

    self.wa_general = nn.Linear(
        # encoder
        self.chatboot_encoder_hidden_size,
        # decoder
        config.chatboot_config.get("hidden_size"),
        bias=False
    )

    self.wa_concat = nn.Linear(
        self.chatboot_encoder_hidden_size+self.chatboot_decoder_hidden_size,
        # decoder
        self.chatboot_decoder_hidden_size,
        bias=False
    )
    self.va = nn.Linear(
        # decoder
        config.chatboot_config.get("hidden_size"),
        1,
    )

def forward(self,hidden_state,encoder_outputs):

    attention_weight = None
    if(self.method=='dot'):
        hidden_state = hidden_state[-1,:,:].permute(1,2,0)
        attention_weight = encoder_outputs.bmm(hidden_state).squeeze(-1)
        attention_weight = F.softmax(attention_weight)

    elif (self.method=='general'):
        encoder_outputs = self.wa_general(encoder_outputs)
        hidden_state = hidden_state[-1:,:,:].permute(1,2,0)
        attention_weight = encoder_outputs.bmm(hidden_state).squeeze(-1)
        attention_weight = F.softmax(attention_weight,dim=-1)

    elif self.method == 'concat':
        hidden_state = hidden_state[-1,:,:].squeeze(0)
        hidden_state = hidden_state.repeat(1,encoder_outputs.size(1),1)
        concated = torch.cat([hidden_state,encoder_outputs],dim=-1)
        batch_size = encoder_outputs.size(0)
        encoder_seq_len = encoder_outputs.size(1)
        attention_weight = self.va(F.tanh(self.wa_concat(concated.view((batch_size*encoder_seq_len,-1))))).sequeeze(-1)
        attention_weight = F.softmax(attention_weight.view(batch_size,encoder_seq_len))

    assert attention_weight!=None,"error attention_weight can't be None"

    return attention_weight

```

decoder與beamsearch

這個東西的話和我們的decoder是在一起的,同時我們的注意力機制其實也是在這裡的。 在這裡插入圖片描述 那麼實現的話是這樣的:

```python import torch.nn as nn import torch.nn.functional as F from config import config import torch import random from utils.drive import getDrive from boot.chatboot.attention import LuongAttention import heapq

""" in there we import luong Attention """

class Beam(object):

def __init__(self):
    self.heap = list()
    self.beam_width = config.chatboot_config.get("beam_width")

def add(self, probility, complete, seq, decoder_input, decoder_hidden):
    """
    :param probility:
    :param complete: is or not eos
    :param seq: all token list
    :param decoder_input:
    :param decoder_hidden:
    :return:
    """

    heapq.heappush(self.heap, [probility, complete, seq, decoder_input, decoder_hidden])

    if (len(self.heap) > self.beam_width):
        heapq.heappop(self.heap)

def __iter__(self):
    return iter(self.heap)

class Decoder(nn.Module): def init(self,by_word=False): super(Decoder,self).init()

    self.drive = getDrive()

    """
    attention init 
    """

    if(config.chatboot_config.get("use_attention")):


        self.chatboot_encoder_hidden_size = config.chatboot_config.get("hidden_size")*2 if config.chatboot_config.get(
                "bidirectional") else config.chatboot_config.get("hidden_size")
        self.chatboot_decoder_hidden_size = config.chatboot_config.get("hidden_size")*2 if config.chatboot_config.get(
                "bidirectional") else config.chatboot_config.get("hidden_size")

        self.atte = LuongAttention()
        self.wa_concat = nn.Linear(
            self.chatboot_encoder_hidden_size+self.chatboot_decoder_hidden_size,
            # decoder
            self.chatboot_decoder_hidden_size,
            bias=False
        )

    if(by_word):
        self.target_ws = config.chatboot_config_load.get("word_corpus_by_word_target_load")
    else:
        self.target_ws = config.chatboot_config_load.get("word_corpus_no_by_word_target_load")

    self.embedding = nn.Embedding(
        num_embeddings=len(self.target_ws),
        embedding_dim=config.chatboot_config.get("embedding_dim"),
        padding_idx=config.chatboot_config.get("padding_idx")
    )

    self.gru = nn.GRU(input_size=config.chatboot_config.get("embedding_dim"),
                      dropout=config.chatboot_config.get("dropout"),
                      num_layers=config.chatboot_config.get("num_layers"),
                      hidden_size=config.chatboot_config.get("hidden_size"),
                      bidirectional=config.chatboot_config.get("bidirectional"),
                      batch_first=config.chatboot_config.get("batch_first")
                      )

    self.fc = nn.Linear(config.chatboot_config.get("hidden_size")*
                        config.chatboot_config.get("num_layers"),
                        len(self.target_ws)
                        )

def forward(self,target_data,encoder_hidden,encoder_outputs):
    """
    :param target_data:
    :param encoder_hidden:

    The hardest thing to do here is to pay attention to the dimensional
    changes in input and publication.
    :return:
    """

    decoder_hidden = encoder_hidden
    batch_size = target_data.size(0)

    """
    sos input in decoder for first time step
    """
    decoder_input = torch.LongTensor(torch.ones([batch_size,1],dtype=torch.int64
                                                ))*config.chatboot_config.get("sos_idx")
    decoder_input = decoder_input.to(self.drive)

    decoder_outputs = torch.zeros([batch_size,config.chatboot_config.get("target_max_len")+1,
                                   len(self.target_ws)
                                   ]).to(self.drive)


    if (random.random() < config.chatboot_config.get("teacher_forcing_ratio")):

        for time in range(config.chatboot_config.get("target_max_len") + 1):
            decoder_output_t, decoder_hidden = self.forward_step(decoder_input, decoder_hidden,encoder_outputs)
            decoder_outputs[:, time, :] = decoder_output_t
            decoder_input = target_data[:,time].unsqueeze(-1)
    else:
        for time in range(config.chatboot_config.get("target_max_len")+1):
            decoder_output_t,decoder_hidden = self.forward_step(decoder_input,decoder_hidden,encoder_outputs)
            decoder_outputs[:,time,:] = decoder_output_t

            value,index = torch.topk(decoder_output_t,1)
            decoder_input = index

    return decoder_outputs,decoder_hidden


def forward_step(self,decoder_input, decoder_hidden,encoder_outputs):

    decoder_input_embeded = self.embedding(decoder_input)
    out,decoder_hidden = self.gru(decoder_input_embeded,decoder_hidden)
    """
    there we add attention way
    """
    """*******************************************************"""
    if (config.chatboot_config.get("use_attention")):

        attention_weight = self.atte(decoder_hidden,encoder_outputs).unsqueeze(1)

        context_vector = attention_weight.bmm(encoder_outputs)

        concated = torch.cat([out,context_vector],dim=-1).squeeze(1)

        out = torch.tan(self.wa_concat(concated))
        """*******************************************************"""
        # out = out.squeeze(1)
    else:
        out = out.squeeze(1)
    out = self.fc(out)
    output = F.log_softmax(out,dim=-1)
    return output,decoder_hidden

def evaluate(self,encoder_hidden,encoder_outputs):

    decoder_hidden = encoder_hidden
    batch_size = encoder_hidden.size(1)

    decoder_input = torch.LongTensor(torch.ones([batch_size,1],dtype=torch.int64
                                                ))*config.chatboot_config.get("sos_idx")
    decoder_input = decoder_input.to(self.drive)
    indices = []

    for i in range(config.chatboot_config.get("out_seq_len")):

        decoder_output_t,decoder_hidden = self.forward_step(decoder_input,decoder_hidden,encoder_outputs)
        value,index = torch.topk(decoder_output_t,1)
        decoder_input = index
        indices.append(index.squeeze(-1).cpu().detach().numpy())

    return indices

def evaluate_beamsearch(self,encoder_hidden,encoder_outputs):
    batch_size = encoder_hidden.size(1)

    decoder_input = torch.LongTensor([[config.chatboot_config.get("sos_idx")]*batch_size]).to(self.drive)
    decoder_hidden = encoder_hidden

    prev_beam = Beam()
    prev_beam.add(1,False,[decoder_input],decoder_input,decoder_hidden)
    while True:
        cur_beam = Beam()
        for _probility,_complete,_seq,_decoder_input,_decoder_hidden in prev_beam:
            if(_complete==True):
                cur_beam.add(_probility,_complete,_seq,_decoder_input,_decoder_hidden)
            else:
                decoder_output_t,decoder_hidden = self.forward_step(_decoder_input,_decoder_hidden,encoder_outputs)

                value,index = torch.topk(decoder_output_t,config.chatboot_config.get("beam_width"))

                for m,n in zip(value[0],index[0]):
                    decoder_input = torch.LongTensor([[n]]).to(self.drive)
                    seq = _seq+[n]
                    probility = _probility * m
                    if(n.item()==config.chatboot_config.get("eos_idx")):
                        complete = True
                    else:
                        complete = False

                        cur_beam.add(probility,complete,seq,decoder_input,decoder_hidden)

        best_prob,best_complete,best_seq,_,_ = max(cur_beam)
        if(best_complete==True or len(best_seq)-1 == config.chatboot_config.get("out_seq_len")):
            return self.__prepar_seq(best_seq)
        else:
            prev_beam = cur_beam

def __prepar_seq(self,best_seq):
    if(best_seq[0].item()==config.chatboot_config.get("sos_idx")):
        best_seq = best_seq[1:]
    if(best_seq[-1].item()==config.chatboot_config.get("eos_idx")):
        best_seq = best_seq[:-1]
    best_seq = [i.item() for i in best_seq]
    return best_seq

``` 這裡面最難的其實還是關於它裡面資料維度的一個變化,原理其實還是比較簡單的。

載入驅動

那麼在這裡的話還有一個細節就是回到工具包:

在這裡插入圖片描述

這裡的還有這個玩意。

```python import torch from config import config

def getDrive(): if (torch.cuda.is_available()): if (not config.chatboot_config.get("drive") == 'cpu'): div = "cuda:" + config.chatboot_config.get("drive") drive = torch.device(div) else: drive = torch.device("cpu") else: drive = torch.device("cpu") return drive ```

Seq2Seq搭建

現在的話我們的幾個重要部件都實現了,那麼我們現在需要組裝一下了。 在這裡插入圖片描述

```python

from torch import nn from boot.chatboot.decoder import Decoder from boot.chatboot.encoder import Encoder from utils.drive import getDrive from config import config

class Seq2Seq(nn.Module):

def __init__(self):
    super(Seq2Seq,self).__init__()

    self.drive = getDrive()
    self.encoder = Encoder().to(self.drive)
    self.decoder = Decoder().to(self.drive)


def forward(self,input_data,target_data,input_length,target_length):

    encoder_outputs,encoder_hidden = self.encoder(input_data,input_length)
    decoder_outputs,decoder_hidden = self.decoder(target_data,encoder_hidden,encoder_outputs)

    return decoder_outputs,decoder_hidden

def evaluate(self,input_data,input_length):
    encoder_outputs,encoder_hidden = self.encoder(input_data,input_length)
    if(config.chatboot_config.get("beam_search")):
        indices = self.decoder.evaluate(encoder_hidden,encoder_outputs)
    else:
        indices = self.decoder.evaluate_beamsearch(encoder_hidden,encoder_outputs)
    return indices

```

訓練

那麼到了最後就是咱們的訓練了 在這裡插入圖片描述 那麼這個時候我需要說的就是我們的這個玩意有點類似於分類,但是和分類的區別是,並不是在訓練集的時候損失越小越好,我們在分類的時候是損失越小那麼就越準,但是在這裡太準了就容易出事,就比如有這樣的對話,你說:“你好”,然後在咱們的回答是:“你好呀””。這個時候你相當於分類,網路生成了“你好呀”這句話是沒問題,但是它生成了:“你也好呀”,或者是:“你吃了嗎”。這種對話也是沒問題的,但是單純作為分類的話,那麼如果生成的是這兩句話中的其中一個的話,那麼從分類的結果上來說,他是匹配句子當中每一個詞的id。那麼損失是相當難看的,可是實際對話效果可能又是不錯的。因此這也是比較難驗證的。所以雖然他也算是有監督的,但是和影象這種不一樣,他不是完全對應的。也就是沒有標準答案,這個也是問題,當然解決也是可以的那就是資料集,多個答案,但是這個難度比較大,咱們這裡做也不現實。所以的話在這塊也是區別於分類我們還會搞一個驗證集去判斷對了幾個,咱們這不好判斷,因為語言它不是問答,而且問答的話是做匹配。

這部分的實現比較簡單

```python from boot.chatboot.chat_dataset import Chat_dataset,collate_fn from boot.chatboot.seq2seq import Seq2Seq from torch.optim import Adam from torch.utils.data import DataLoader,Dataset import torch.nn.functional as F from config import config from tqdm import tqdm import torch.nn as nn import torch from utils.drive import getDrive

class Train_model(object): def init(self,by_word=False):

    if(config.chatboot_config.get("use_attention")):
        print("\033[0;32;40m using attention by {} method !\033[0m".format(
            config.chatboot_config.get("attention_method")
        ))

    self.drive = getDrive()
    self.seq2seq = Seq2Seq()
    self.seq2seq = self.seq2seq.to(self.drive)
    self.optimizer = Adam(self.seq2seq.parameters(),lr=0.001)
    self.train_data_loader = DataLoader(Chat_dataset(),
                                        batch_size=config.chatboot_config.get("batch_size"),
                                        shuffle=True,
                                        num_workers=config.chatboot_config.get("num_workers"),
                                        collate_fn=collate_fn)

    if(by_word):
        self.save_seq2seq = config.chatboot_config.get("seq2seq_model_by_word")
        self.save_optimizer = config.chatboot_config.get("optimizer_model_by_word")
    else:
        self.save_seq2seq = config.chatboot_config.get("seq2seq_model_no_by_word")
        self.save_optimizer = config.chatboot_config.get("optimizer_model_no_by_word")

def train(self,e):
    self.drive = getDrive()
    bar = tqdm(enumerate(self.train_data_loader),
               total=len(self.train_data_loader),desc="training",
               colour='green'
               )
    e_loss = 0
    for idx, (input_data, target_data, input_length, target_length) in bar:

        input_data = input_data.to(self.drive)
        target_data = target_data.to(self.drive)
        input_length = input_length.to(self.drive)
        target_length = target_length.to(self.drive)

        self.optimizer.zero_grad()
        decoder_outputs,decoder_hidden = self.seq2seq(input_data,target_data,
                                                      input_length,target_length
                                                      )


        decoder_outputs = decoder_outputs.reshape(decoder_outputs.size(0)*decoder_outputs.size(1),-1)

        target_data = target_data.view(-1)
        loss = F.nll_loss(decoder_outputs,target_data,
                          ignore_index=config.chatboot_config.get("padding_idx")
                          )

        loss.backward()
        nn.utils.clip_grad_norm_(self.seq2seq.parameters(),max_norm=config.chatboot_config.get("max_norm"))
        self.optimizer.step()
        e_loss+=loss.item()
        bar.set_description("drive:{} \t epoch:{} \t idx:{} \t current_batch_loss:{:.2f}".format(self.drive,e,idx,loss.item()))

    print("\n","\033[0;32;40m drive:{} \t epoch:{}  \t current_epoch_loss:{:.2f}\033[0m".format(self.drive, e, e_loss))
    if(e%2==0):
        torch.save(self.seq2seq.state_dict(),self.save_seq2seq)
        torch.save(self.optimizer.state_dict(),self.save_optimizer)

if name == 'main': train_model = Train_model() for e in range(1, 5): train_model.train(e)

```

當我們訓練完成之後,我們將得到權重檔案。我們這裡搭建的是一個兩個雙向的2層的GRU加上全連線。得到的權重模型大概是70MB。

那麼執行完畢之後的話你講得到這兩個檔案: 在這裡插入圖片描述 同樣的,我們的訓練可以基於分詞(準確的說是分字),也可以基於那個jieba分詞的結果來,這個的話有個關鍵引數叫做by_word這個改為TRUE就是按照分字來一下了,然後你看看效果就好了。

預測

先說一下,我們的配置是GTX1650 4GB,跑一次訓練需要12分鐘。也就是說訓練10次2個小時沒了。所以我這裡演示的效果不是很好,沒辦訓練的問題,當然還有引數的調優之類的,這個的話需要各位自己拿到專案之後去訓練了,而且相關資料檔案比較大,所以都不會上傳,各位下載好開頭給的資原始檔後,放到指定位置,先點選訓練,他自己會生成很多檔案,之後完成訓練。這個大家應該是看到了的。

```python from boot.chatboot.chat_dataset import Chat_dataset,collate_fn from boot.chatboot.seq2seq import Seq2Seq from config import config from utils.drive import getDrive from utils.cut_word import Cut import torch import numpy as np

class Eval_model(object):

def __init__(self,by_word=False):
    self.by_word = by_word
    self.drive = getDrive()
    self.seq2seq = Seq2Seq()
    self.seq2seq = self.seq2seq.to(self.drive)
    self.cut = Cut()
    if(by_word):
        self.seq2seq.load_state_dict(torch.load(config.chatboot_config.get("seq2seq_model_by_word")))
        self.input_ws = config.chatboot_config_load.get("word_corpus_by_word_input_load")
        self.target_ws = config.chatboot_config_load.get("word_corpus_by_word_target_load")

    else:
        self.seq2seq.load_state_dict(torch.load(config.chatboot_config.get("seq2seq_model_no_by_word")))
        self.input_ws = config.chatboot_config_load.get("word_corpus_no_by_word_target_load")
        self.target_ws = config.chatboot_config_load.get("word_corpus_no_by_word_target_load")

def while_talk(self):
    while True:
        input_data = input("please input:")
        input_data = self.cut.cut(input_data,by_word=self.by_word)
        if len(input_data) < config.chatboot_config.get( "input_max_len"):
            input_length = len(input_data)
        else:
            input_length = config.chatboot_config.get("input_max_len")

        input_data = [self.input_ws.transform(input_data, max_len=config.chatboot_config.get("input_max_len"))]
        input_data = torch.LongTensor(input_data).to(self.drive)
        input_length = torch.LongTensor([input_length]).to(self.drive)
        """
        index-->Plural form
        """
        indices = np.array(self.seq2seq.evaluate(input_data,input_length)).flatten()

        outputs = self.target_ws.inverse_transform(indices)

        print("xiaojiejie:","".join(outputs))

if name == 'main': eval_model = Eval_model() eval_model.while_talk()

``` 那麼此時的話,拿到這個,或者封裝一下就OK了。 效果大概是這個樣子的,這個自己慢慢訓練一波,調調引數啥的,或者再優化一下資料集。 在這裡插入圖片描述

總結

那麼這個的話就是我們聊天AI的搭建了,那麼如果你想要訓練一個小姐姐AI,記住,你的資料集裡面,那個target得是小姐姐的話語,如果不是我不保證。當然這個東西現在存在的問題還是挺多的,如果要我選,我選擇做好的,因為訓練調優的話還是需要時間磨合的。但是作為一個baseline日後不斷優化是不錯的選擇。同時自己動手豐衣足食,你完全可以拿你女朋友和你的聊天記錄處理一下作為語料自己玩玩嘛。OK,這個就是咱們擴寫之後的比較完整的一個部分了。建議可以深入瞭解的去看看本文提到的東西然後在看看這個程式碼。同時的話其實你發現了,這玩意可以做其他的一個生成,例如我把input變成古詩題目,target作為詩句,那麼是不是可以實現一個古詩生成,當然得做調整。但總體上他是輸入一個序列得到一個序列的結構,比如翻譯之類的,這個結構的話也是比較適應的,之後就是如何優化,比如transform其實就是這個結構,加了很多注意力機制。同時的話,對於這種序列尤其是這樣大量二維矩陣的運算,我們是不是還可以加入CNN去做,都是一個優化方案,也確實有這樣的大哥在幹。

OK,到這裡恭喜你看到這裡,結束了,太酷了: 在這裡插入圖片描述 最後一句話,好好學習,天天向上!!!來個小姐姐送我一張RTX4080 評論區踹我(手動狗頭)