基於Seq2Seq模型的簡易中文聊天機器人

臨近畢業季,又想起了做過的簡易聊天機器人chartbot畢業設計,因爲算是自己第一次接觸這個智能問答領域吧,所以到現在還覺得特別有意思且難忘。我是個行動派,覺得有意思的東西,肯定就要記錄下來了。下面我會簡要敘述當時我的一些思路以及注意事項,希望對大家有所啓示。

1. 解決的實際問題

本項目類似於知識問答系統,就是用戶任意輸入一段話,我們的系統會自動生成回覆內容,並在界面中展示出來。廢話不多說,先上圖吧!

可以看到簡單的對話功能是實現了,和Siri語音對話助手差不多,當然了,功能沒那麼強大哈!這個不能吹牛b。

2. 工作環境配置

配置環境是你寫代碼的第一步,也是最基礎的一步。很多人會在配置環境這一塊崩潰掉,絕對不包括我哈哈哈哈哈。

1) Python3.6 (雖然現在都到了3.7了,但我仍是覺得3.6是目前比較穩定的一個版本,2.7版本都要捨棄了,所以大家不要再使用這個版本了)
2) Pytorch1.4 (建議先裝好Anaconda環境,再在pytorch官網用conda命令安裝1.4版本的pytorch. 用pip安裝總是報bug,親身經歷哈!最後,安裝的時候如果網速奇差,反正我每次都是這樣,建議直接下載.whl安裝包,然後用pip命令在安裝包目錄下安裝,不然你可能會崩潰掉的,相信我,10k的網速你不會同意的)
3) jieba (這是個分詞包,版本沒限制,好像沒出什麼問題)
4) torchnet (這個包也是一樣的噁心,直接用pip或conda命令在dos界面安裝的話,多半是安裝到一半給你希望然後直接崩潰報錯的,凡是碰到此類狀況,聽我的,什麼也不要做,砸電腦就是了。哈哈哈哈哈哈開個玩笑哈,直接在網上搜torchnet安裝包吧,離線下載了只能)
5) fire (這個包沒什麼限制,正常裝上去即可)

最後,我還是想提下,pytorch版本分爲CPU版和GPU版。CPU版就是普通的,只在你筆記本上跑,你也不用管什麼。有錢的科研大佬自然就懂GPU版本的,比如像我咳咳咳,不要臉又來了哈哈哈。有條件的還是下載GPU版本的Pytorch吧,否則後面訓練預料的話電腦基本上會卡住,當然想偷懶只下CPU版本的也行,我會提供一個訓練好的模型給大家。

3. 數據預處理

由於我做的是中文聊天機器人,所以選用的訓練語料也是中文的。中文的預料集也有很多種類的,大的語料集能有幾個G以上的,迷你型的10M左右吧。大的語料集我說過你要是沒有GPU這類的硬件支持的話,那別做夢了。當時我也是這樣慘兮兮的一員,所以就選取了只有10M的青雲預料。當然你也可以選用如小黃鴨這類相當的預料,同樣很小,只是內容風格不一樣,所以最後問答的內容也會有差異。

得到語料後,我們要做的就是將其分詞了,只有分詞了才能表示成詞向量。有些小白可能會問詞向量是什麼,百度去!反正就是像計算機只認0和1二進制數字一樣,我們的神經網絡模型也只認詞向量,懂了吧。

我先貼出我項目的結構圖吧!

數據提取命令爲:

python datapreprocess.py

得到提取好的詞向量之後,我們纔可以訓練模型了。

4. 訓練模型

本系統所採用的的神經網絡模型主要是基於Seq2Seq模型和Attention注意力機制的。因爲機器人聊天系統實際上也相當於機器翻譯功能,用戶輸入給定的語句,然後通過模型生成另一種合適的語句。通常情況下,採用LSTM長短時記憶網絡來解決該問題。此處,我們是採用了LSTM模型的進階版Seq2Seq序列到序列模型。

Seq2Seq模型主要包括Encoder和Decoder兩個基本塊,其中,encoder負責將輸入序列壓縮成指定長度的向量,其中的網絡結構爲兩層雙向GRU模型;而decoder則負責根據語義向量生成指定的序列,這個過程也稱爲解碼,採用了雙層單向GRU模型。又因爲在Seq2seq模型中,原始編解碼模型的encode過程會生成一箇中間向量C,用於保存原序列的語義信息。但是這個向量長度是固定的,當輸入原序列的長度比較長時,向量C無法保存全部的語義信息,上下文語義信息受到了限制,這也限制了模型的理解能力。所以使用Attention機制來打破這種原始編解碼模型對固定向量的限制。Attention機制通俗的講就是把注意力集中放在重要的點上,而忽略其他不重要的因素。

代碼如下:

class EncoderRNN(nn.Module):
    def __init__(self, opt, voc_length):
        super(EncoderRNN, self).__init__()
        self.num_layers = opt.num_layers
        self.hidden_size = opt.hidden_size
        # nn.Embedding輸入向量維度是字典長度,輸出向量維度是詞向量維度
        self.embedding = nn.Embedding(voc_length, opt.embedding_dim)
        # 雙向GRU作爲Encoder
        self.gru = nn.GRU(opt.embedding_dim, self.hidden_size, self.num_layers,
                          dropout=(0 if opt.num_layers == 1 else opt.dropout), bidirectional=opt.bidirectional)

    def forward(self, input_seq, input_lengths, hidden=None):
        
        embedded = self.embedding(input_seq)
        packed = torch.nn.utils.rnn.pack_padded_sequence(embedded, input_lengths)
        outputs, hidden = self.gru(packed, hidden)
        outputs, _ = torch.nn.utils.rnn.pad_packed_sequence(outputs)
        outputs = outputs[:, :, :self.hidden_size] + outputs[:, :, self.hidden_size:]
        return outputs, hidden


class Attn(torch.nn.Module):
    def __init__(self, attn_method, hidden_size):
        super(Attn, self).__init__()
        self.method = attn_method  # attention方法
        self.hidden_size = hidden_size
        if self.method not in ['dot', 'general', 'concat']:
            raise ValueError(self.method, "is not an appropriate attention method.")
        if self.method == 'general':
            self.attn = torch.nn.Linear(self.hidden_size, self.hidden_size)
        elif self.method == 'concat':
            self.attn = torch.nn.Linear(self.hidden_size * 2, self.hidden_size)
            self.v = torch.nn.Parameter(torch.FloatTensor(self.hidden_size))

    def dot_score(self, hidden, encoder_outputs):
        return torch.sum(hidden * encoder_outputs, dim=2)

    def general_score(self, hidden, encoder_outputs):
        energy = self.attn(encoder_outputs)
        return torch.sum(hidden * energy, dim=2)

    def concat_score(self, hidden, encoder_outputs):
        energy = self.attn(torch.cat((hidden.expand(encoder_outputs.size(0), -1, -1),
                                      encoder_outputs), 2)).tanh()
        return torch.sum(self.v * energy, dim=2)

    def forward(self, hidden, encoder_outputs):
        if self.method == 'general':
            attn_energies = self.general_score(hidden, encoder_outputs)
        elif self.method == 'concat':
            attn_energies = self.concat_score(hidden, encoder_outputs)
        elif self.method == 'dot':
            attn_energies = self.dot_score(hidden, encoder_outputs)
        # 得到score,shape爲[max_seq_len, batch_size],然後轉置爲[batch_size, max_seq_len]
        attn_energies = attn_energies.t()
        # 對dim=1進行softmax,然後插入維度[batch_size, 1, max_seq_len]
        return F.softmax(attn_energies, dim=1).unsqueeze(1)


class LuongAttnDecoderRNN(nn.Module):
    def __init__(self, opt, voc_length):
        super(LuongAttnDecoderRNN, self).__init__()

        self.attn_method = opt.method
        self.hidden_size = opt.hidden_size
        self.output_size = voc_length
        self.num_layers = opt.num_layers
        self.dropout = opt.dropout
        self.embedding = nn.Embedding(voc_length, opt.embedding_dim)
        self.embedding_dropout = nn.Dropout(self.dropout)
        self.gru = nn.GRU(opt.embedding_dim, self.hidden_size, self.num_layers,
                          dropout=(0 if self.num_layers == 1 else self.dropout))
        self.concat = nn.Linear(self.hidden_size * 2, self.hidden_size)
        self.out = nn.Linear(self.hidden_size, self.output_size)
        self.attn = Attn(self.attn_method, self.hidden_size)

    def forward(self, input_step, last_hidden, encoder_outputs):
        embedded = self.embedding(input_step)
        embedded = self.embedding_dropout(embedded)
        rnn_output, hidden = self.gru(embedded, last_hidden)
        attn_weights = self.attn(rnn_output, encoder_outputs)
        # bmm批量矩陣相乘,
        context = attn_weights.bmm(encoder_outputs.transpose(0, 1))
        rnn_output = rnn_output.squeeze(0)
        context = context.squeeze(1)
        concat_input = torch.cat((rnn_output, context), 1)
        concat_output = torch.tanh(self.concat(concat_input))
        output = self.out(concat_output)
        output = F.softmax(output, dim=1)

        return output, hidden

好了,模型也巴拉巴拉一大堆了,真的累。雖然大部分是從網上借鑑學習過來的,但是也累啊!大家可別學我啊。模型訓練2000次,最終得到訓練好的模型參數,並保存至checkpoints文件夾下面。執行語句如下所示:

python train_eval.py train

5. 測試模型

得到了訓練好的模型過後,最後一步就是測試模型啦。也是我們最最最期待的一刻啦,終於能和機器人交互了,是不是有點小激動了呢?不激動也得給勞資配合起來哈哈哈哈哈。對了,差點還忘了,我們這個系統還是加了知識庫選項的。知識庫是什麼呢?你讓我組織下,簡單來說,就是像你考試做小抄一樣,提前準備好一些固定的答案,考試時不會做你肯定就是先看小抄吧,小抄上沒有你纔會想着去偷瞄別人的或者自己啃筆頭來計算。不過我們的知識庫也是用的小型的,就一百多個問題對吧,有跟沒有沒多大的差別。

5.1 加入知識庫的對話測試

使用知識庫時, 需要傳入參數use_QA_first=True。此時,對於輸入的字符串,首先在知識庫中匹配最佳的問題和答案,並返回。找不到時,才調用聊天機器人自動生成回覆。這裏的知識庫是爬取整理的騰訊雲官方文檔中的常見問題和答案,100條,僅用於測試!

執行命令爲:python main.py chat --use_QA_first=True.

效果如下:

5.2 不加知識庫的對話測試

調用聊天機器人自動生成回覆

執行命令爲:python main.py chat --use_QA_first=False.

效果如下:

6. 小結

上面就是我畢設的簡要介紹了,其實很感慨,因爲當時碰到了無數噁心的bug,由於沒有人指導,完全是自己一個人扛。就這種感受,不知道你有沒有感受,可能能把人急哭的那種。沒有服務器跑,自己花錢買了一個月的過來用,蒐集各種語料集做比較,有的是國外的語料集,由於國內限速,還得去找國外的朋友幫我下下來然後發給我。害,感謝的人也有很多吧,真的很感謝一路過來幫助過我的人,沒有你們,我到達不了現在的地步,謝謝,後面煽情了,大家可自行忽略,不騙眼淚。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章