<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ??碼云GVP開源項目 12k star Uniapp+ElementUI 功能強大 支持多語言、二開方便! 廣告
                {% raw %} # 十六、使用 RNN 和注意力機制進行自然語言處理 > 譯者:[@SeanCheney](https://www.jianshu.com/u/130f76596b02) 當阿蘭·圖靈在 1950 年設計[圖靈機](https://links.jianshu.com/go?to=http%3A%2F%2Fcogprints.org%2F499%2F1%2Fturing.html)時,他的目標是用人的智商來衡量機器。他本可以用其它方法來測試,比如看圖識貓、下棋、作曲或逃離迷宮,但圖靈選擇了一個語言任務。更具體的,他設計了一個聊天機器人,試圖迷惑對話者將其當做真人。這個測試有明顯的缺陷:一套硬編碼的規則可以愚弄粗心人(比如,機器可以針對一些關鍵詞,做出預先定義的模糊響應;機器人可以假裝開玩笑或喝醉;或者可以通過反問僥幸過關),忽略了人類的多方面的智力(比如非語言交流,比如面部表情,或是學習動手任務)。但圖靈測試強調了一個事實,語言能力是智人最重要的認知能力。我們能創建一臺可以讀寫自然語言的機器嗎? 自然語言處理的常用方法是循環神經網絡。所以接下來會從 character RNN 開始(預測句子中出現的下一個字符),繼續介紹 RNN,這可以讓我們生成一些原生文本,在過程中,我們會學習如何在長序列上創建 TensorFlow Dataset。先使用的是無狀態 RNN(每次迭代中學習文本中的隨機部分),然后創建一個有狀態 RNN(保留訓練迭代之間的隱藏態,可以從斷點繼續,用這種方法學習長規律)。然后,我們會搭建一個 RNN,來做情感分析(例如,讀取影評,提取評價者對電影的感情),這次是將句子當做詞的序列來處理。然后會介紹用 RNN 如何搭建編碼器-解碼器架構,來做神經網絡機器翻譯(NMT)。我們會使用 TensorFlow Addons 項目中的 seq2seq API 。 本章的第二部分,會介紹注意力機制。正如其名字,這是一種可以選擇輸入指定部分,模型在每個時間步都得聚焦的神經網絡組件。首先,會介紹如何使用注意力機制提升基于 RNN 的編碼器-解碼器架構的性能,然后會完全摒棄 RNN,介紹只使用注意力的架構,被稱為 Transformer(轉換器)。最后,會介紹 2018、2019 兩年 NLP 領域的進展,包括強大的語言模型,比如 GPT-2 和 Bert,兩者都是基于 Transformer 的。 先從一個簡單有趣的模型開始,它能寫出莎士比亞風格的文字。 ## 使用 Character RNN 生成莎士比亞風格的文本 在 2015 年一篇著名的、名為《The Unreasonable Effectiveness of Recurrent Neural Networks》博客中,Andrej Karpathy 展示了如何訓練 RNN,來預測句子中的下一個字符。這個 Char-RNN 可以用來生成小說,每次一個字符。下面是一段簡短的、由 Char-RNN 模型(在莎士比亞全部著作上訓練而成)生成的文本: ```py PANDARUS: Alas, I think he shall be come approached and the day When little srain would be attain’d into being never fed, And who is but a chain and subjects of his death, I should not sleep. ``` 雖然文筆一般,但只是通過學習來預測一句話中的下一個字符,模型在單詞、語法、斷句等等方面做的很好。接下來一步一步搭建 Char-RNN,從創建數據集開始。 ### 創建訓練數據集 首先,使用 Keras 的`get_file()`函數,從 Andrej Karpathy 的 [Char-RNN 項目](https://links.jianshu.com/go?to=https%3A%2F%2Fgithub.com%2Fkarpathy%2Fchar-rnn),下載所有莎士比亞的作品: ```py shakespeare_url = "https://homl.info/shakespeare" # shortcut URL filepath = keras.utils.get_file("shakespeare.txt", shakespeare_url) with open(filepath) as f: shakespeare_text = f.read() ``` 然后,將每個字符編碼為一個整數。方法之一是創建一個自定義預處理層,就像之前在第 13 章做的那樣。但在這里,使用 Keras 的`Tokenizer`會更加簡單。首先,將一個將 tokenizer 擬合到文本:tokenizer 能從文本中發現所有的字符,并將所有字符映射到不同的字符 ID,映射從 1 開始(注意不是從 0 開始,0 是用來做遮擋的,后面會看到): ```py tokenizer = keras.preprocessing.text.Tokenizer(char_level=True) tokenizer.fit_on_texts([shakespeare_text]) ``` 設置`char_level=True`,以得到字符級別的編碼,而不是默認的單詞級別的編碼。這個 tokenizer 默認將所有文本轉換成了小寫(如果不想這樣,可以設置`lower=False`)。現在 tokenizer 可以將一整句(或句子列表)編碼為字符 ID 列表,這可以告訴我們文本中有多少個獨立的字符,以及總字符數: ```py >>> tokenizer.texts_to_sequences(["First"]) [[20, 6, 9, 8, 3]] >>> tokenizer.sequences_to_texts([[20, 6, 9, 8, 3]]) ['f i r s t'] >>> max_id = len(tokenizer.word_index) # number of distinct characters >>> dataset_size = tokenizer.document_count # total number of characters ``` 現在對完整文本做編碼,將每個字符都用 ID 來表示(減 1 使 ID 從 0 到 38,而不是 1 到 39): ```py [encoded] = np.array(tokenizer.texts_to_sequences([shakespeare_text])) - 1 ``` 繼續之前,需要將數據集分成訓練集、驗證集和測試集。不能大論字符,該怎么處理這種序列式的數據集呢? ### 如何切分序列數據集 避免訓練集、驗證集、測試集發生重合非常重要。例如,可以取 90%的文本作為訓練集,5%作為驗證集,5%作為測試集。在這三個數據之間留出空隙,以避免段落重疊也是非常好的主意。 當處理時間序列時,通常按照時間切分:例如,可以將從 2000 到 2012 的數據作為訓練集,2013 年到 2015 年作為驗證集,2016 年到 2018 年作為測試集。但是,在另一些任務中,可以按照其它維度來切分,可以得到更長的時間周期進行訓練。例如,10000 家公司從 2000 年到 2018 年的金融健康數據,可以按照不同公司來切分。但是,很可能其中一些公司是高度關聯的(比如,經濟領域的公司漲落相同),如果訓練集和測試集中有關聯的公司,則測試集的意義就不大,泛化誤差會存在偏移。 因此,在時間維度上切分更加安全 —— 但這實際是默認 RNN 可以(在訓練集)從過去學到的規律也適用于將來。換句話說,我們假設時間序列是靜態的(至少是在一個較寬的區間內)。對于時間序列,這個假設是合理的(比如,化學反應就是這樣,化學定理不會每天發生改變),但其它的就不是(例如,金融市場就不是靜態的,一旦交易員發現規律并從中牟利,規律就會改變)。要保證時間序列確實是靜態的,可以在驗證集上畫出模型隨時間的誤差:如果模型在驗證集的前端表現優于后段,則時間序列可能就不夠靜態,最好是在一個更短的時間區間內訓練。 總而言之,將時間序列切分成訓練集、驗證集和測試集不是簡單的工作,怎么做要取決于具體的任務。 回到莎士比亞!這里將前 90%的文本作為訓練集(剩下的作為驗證集和測試集),創建一個`tf.data.Dataset`,可以從這個集和一個個返回每個字符: ```py train_size = dataset_size * 90 // 100 dataset = tf.data.Dataset.from_tensor_slices(encoded[:train_size]) ``` ### 將序列數據集切分成多個窗口 現在訓練集包含一個單獨的長序列,超過 100 萬的任務,所以不能直接在這個訓練集上訓練神經網絡:現在的 RNN 等同于一個有 100 萬層的深度網絡,只有一個超長的單實例來訓練。所以,得使用數據集的`window()`方法,將這個長序列轉化為許多小窗口文本。每個實例都是完整文本的相對短的子字符串,RNN 只在這些子字符串上展開。這被稱為截斷沿時間反向傳播。調用`window()`方法創建一個短文本窗口的數據集: ```py n_steps = 100 window_length = n_steps + 1 # target = input 向前移動 1 個字符 dataset = dataset.window(window_length, shift=1, drop_remainder=True) ``` > 提示:可以調節`n_steps`:用短輸入序列訓練 RNN 更為簡單,但肯定的是 RNN 學不到任何長度超過`n_steps`的規律,所以`n_steps`不要太短。 默認情況下,`window()`方法創建的窗口是不重疊的,但為了獲得可能的最大訓練集,我們設定`shift=1`,好讓第一個窗口包含字符 0 到 100,第二個窗口包含字符 1 到 101,等等。為了確保所有窗口是準確的 101 個字符長度(為了不做填充而創建批次),設置`drop_remainder=True`(否則,最后的 100 個窗口會包含 100 個字符、99 個字符,一直到 1 個字符)。 `window()`方法創建了一個包含窗口的數據集,每個窗口也是數據集。這是一個嵌套的數據集,類似于列表的列表。當調用數據集方法處理(比如、打散或做批次)每個窗口時,這樣會很方便。但是,不能直接使用嵌套數據集來訓練,因為模型要的輸入是張量,不是數據集。因此,必須調用`flat_map()`方法:它能將嵌套數據集轉換成打平的數據集。例如,假設 {1, 2, 3} 表示包含張量 1、2、3 的序列。如果將嵌套數據集 {{1, 2}, {3, 4, 5, 6}} 打平,就會得到 {1, 2, 3, 4, 5, 6} 。另外,`flat_map()`方法可以接收函數作為參數,可以處理嵌套數據集的每個數據集。例如,如果將函數 `lambda ds: ds.batch(2)` 傳遞給 `flat_map()` ,它能將 {{1, 2}, {3, 4, 5, 6}} 轉變為 {[1, 2], [3, 4], [5, 6]} :這是一個張量大小為 2 的數據集。 有了這些知識,就可以打平數據集了: ```py dataset = dataset.flat_map(lambda window: window.batch(window_length)) ``` 我們在每個窗口上調用了`batch(window_length)`:因為所有窗口都是這個長度,對于每個窗口,都能得到一個獨立的張量。現在的數據集包含連續的窗口,每個有 101 個字符。因為梯度下降在訓練集中的實例獨立同分布時的效果最好,需要打散這些窗口。然后我們可以對窗口做批次,分割輸入(前 100 個字符)和目標(最后一個字符): ```py batch_size = 32 dataset = dataset.shuffle(10000).batch(batch_size) dataset = dataset.map(lambda windows: (windows[:, :-1], windows[:, 1:])) ``` 圖 16-1 總結了數據集準備步驟(窗口長度是 11,不是 101,批次大小是 3,不是 32)。 ![](https://img.kancloud.cn/3d/9b/3d9b8ce3ad842be52991184527196b3b_1441x664.png)圖 16-1 準備打散窗口的數據集 第 13 章討論過,類型輸入特征通常都要編碼,一般是獨熱編碼或嵌入。這里,使用獨熱編碼,因為獨立字符不多(只有 39): ```py dataset = dataset.map( lambda X_batch, Y_batch: (tf.one_hot(X_batch, depth=max_id), Y_batch)) ``` 最后,加上預提取: ```py dataset = dataset.prefetch(1) ``` 就是這樣!準備數據集是最麻煩的部分。下面開始搭建模型。 ### 搭建并訓練 Char-RNN 模型 根據前面的 100 個字符預測下一個字符,可以使用一個 RNN,含有兩個 GRU 層,每個 128 個單元,每個單元對輸入(`dropout`)和隱藏態(`recurrent_dropout`)的丟失率是 20%。如果需要的話,后面可以微調這些超參數。輸出層是一個時間分布的緊密層,有 39 個單元(`max_id`),因為文本中有 39 個不同的字符,需要輸出每個可能字符(在每個時間步)的概率。輸出概率之后應為 1,所以使用 softmax 激活很熟。然后可以使用`"sparse_categorical_crossentropy"`損失和 Adam 優化器,編譯模型。最后,就可以訓練模型幾個周期了(訓練過程可能要幾個小時,取決于硬件): ```py model = keras.models.Sequential([ keras.layers.GRU(128, return_sequences=True, input_shape=[None, max_id], dropout=0.2, recurrent_dropout=0.2), keras.layers.GRU(128, return_sequences=True, dropout=0.2, recurrent_dropout=0.2), keras.layers.TimeDistributed(keras.layers.Dense(max_id, activation="softmax")) ]) model.compile(loss="sparse_categorical_crossentropy", optimizer="Adam") history = model.fit(dataset, epochs=20) ``` ### 使用 Char-RNN 模型 現在就有了可以預測莎士比亞要寫的下一個人物的模型了。輸入數據之前,先要像之前那樣做預處理,因此寫個小函數來做預處理: ```py def preprocess(texts): X = np.array(tokenizer.texts_to_sequences(texts)) - 1 return tf.one_hot(X, max_id) ``` 現在,用這個模型預測文本中的下一個字母: ```py >>> X_new = preprocess(["How are yo"]) >>> Y_pred = model.predict_classes(X_new) >>> tokenizer.sequences_to_texts(Y_pred + 1)[0][-1] # 1st sentence, last char 'u' ``` 預測成功!接下來用這個模型生成文本。 ### 生成假莎士比亞文本 要使用 Char-RNN 生成新文本,我們可以給模型輸入一些文本,讓模型預測出下一個字母,將字母添加到文本的尾部,再將延長后的文本輸入給模型,預測下一個字母,以此類推。但在實際中,這會導致相同的單詞不斷重復。相反的,可以使用`tf.random.categorical()`函數,隨機挑選下一個字符,概率等同于估計概率。這樣就能生成一些多樣且有趣的文本。根據類的對數概率(logits),`categorical()`函數隨機從類索引采樣。為了對生成文本的多樣性更可控,我們可以用一個稱為“溫度“的可調節的數來除以對數概率:溫度接近 0,會利于高概率字符,而高溫度會是所有字符概率相近。下面的`next_char()`函數使用這個方法,來挑選添加進文本中的字符: ```py def next_char(text, temperature=1): X_new = preprocess([text]) y_proba = model.predict(X_new)[0, -1:, :] rescaled_logits = tf.math.log(y_proba) / temperature char_id = tf.random.categorical(rescaled_logits, num_samples=1) + 1 return tokenizer.sequences_to_texts(char_id.numpy())[0] ``` 然后,可以寫一個小函數,重復調用`next_char()`: ```py def complete_text(text, n_chars=50, temperature=1): for _ in range(n_chars): text += next_char(text, temperature) return text ``` 現在就可以生成一些文本了!先嘗試下不同的溫度數: ```py >>> print(complete_text("t", temperature=0.2)) the belly the great and who shall be the belly the >>> print(complete_text("w", temperature=1)) thing? or why you gremio. who make which the first >>> print(complete_text("w", temperature=2)) th no cce: yeolg-hormer firi. a play asks. fol rusb ``` 顯然,當溫度數接近 1 時,我們的莎士比亞模型效果最好。為了生成更有信服力的文字,可以嘗試用更多`GRU`層、每層更多的神經元、更長的訓練時間,添加正則(例如,可以在`GRU`層中設置`recurrent_dropout=0.3`)。另外,模型不能學習長度超過`n_steps`(只有 100 個字符)的規律。你可以使用更大的窗口,但也會讓訓練更為困難,甚至 LSTM 和 GRU 單元也不能處理長序列。另外,還可以使用有狀態 RNN。 ### 有狀態 RNN 到目前為止,我們只使用了無狀態 RNN:在每個訓練迭代中,模型從全是 0 的隱藏狀態開始訓練,然后在每個時間步更新其狀態,在最后一個時間步,隱藏態就被丟掉,以后再也不用了。如果讓 RNN 保留這個狀態,供下一個訓練批次使用如何呢?這么做的話,盡管反向傳播只在短序列傳播,模型也可以學到長時規律。這被稱為有狀態 RNN。 首先,有狀態 RNN 只在前一批次的序列離開,后一批次中的對應輸入序列開始的情況下才有意義。所以第一件要做的事情是使用序列且沒有重疊的輸入序列(而不是用來訓練無狀態 RNN 時的打散和重疊的序列)。當創建`Dataset`時,調用`window()`必須使用`shift=n_steps`(而不是`shift=1`)。另外,不能使用`shuffle()`方法。但是,準備有狀態 RNN 數據集的批次會麻煩些。事實上,如果調用`batch(32)`,32 個連續的窗口會放到一個相同的批次中,后面的批次不會接著這些窗口。第一個批次含有窗口 1 到 32,第二個批次批次含有窗口 33 到 64,因此每個批次中的第一個窗口(窗口 1 和 33),它們是不連續的。最簡單辦法是使用只包含一個窗口的“批次”: ```py dataset = tf.data.Dataset.from_tensor_slices(encoded[:train_size]) dataset = dataset.window(window_length, shift=n_steps, drop_remainder=True) dataset = dataset.flat_map(lambda window: window.batch(window_length)) dataset = dataset.batch(1) dataset = dataset.map(lambda windows: (windows[:, :-1], windows[:, 1:])) dataset = dataset.map( lambda X_batch, Y_batch: (tf.one_hot(X_batch, depth=max_id), Y_batch)) dataset = dataset.prefetch(1) ``` 圖 16-2 展示了處理的第一步。 ![](https://img.kancloud.cn/41/20/4120298717d42b45022250c5e5c38210_1441x584.png)圖 16-2 為有狀態 RNN 準備連續序列片段的數據集 做批次雖然麻煩,但可以實現。例如,我們可以將莎士比亞作品切分成 32 段等長的文本,每個做成一個連續序列的數據集,最后使用`tf.train.Dataset.zip(datasets).map(lambda *windows: tf.stack(windows))`來創建合適的連續批次,批次中的 n<sup>th</sup>輸入序列緊跟著 n<sup>th</sup>結束的地方(notebook 中有完整代碼)。 現在創建有狀態 RNN。首先,創建每個循環層時需要設置`stateful=True`。第二,有狀態 RNN 需要知道批次大小(因為要為批次中的輸入序列保存狀態),所以要在第一層中設置`batch_input_shape`參數。不用指定第二個維度,因為不限制序列的長度: ```py model = keras.models.Sequential([ keras.layers.GRU(128, return_sequences=True, stateful=True, dropout=0.2, recurrent_dropout=0.2, batch_input_shape=[batch_size, None, max_id]), keras.layers.GRU(128, return_sequences=True, stateful=True, dropout=0.2, recurrent_dropout=0.2), keras.layers.TimeDistributed(keras.layers.Dense(max_id, activation="softmax")) ]) ``` 在每個周期之后,回到文本開頭之前,需要重設狀態。要這么做,可以使用一個小調回: ```py class ResetStatesCallback(keras.callbacks.Callback): def on_epoch_begin(self, epoch, logs): self.model.reset_states() ``` 現在可以編譯、訓練模型了(周期數更多,是因為每個周期比之前變短了,每個批次只有一個實例): ```py model.compile(loss="sparse_categorical_crossentropy", optimizer="Adam") model.fit(dataset, epochs=50, callbacks=[ResetStatesCallback()]) ``` > 提示:訓練好模型之后,只能預測訓練時相同大小的批次。為了避免這個限制,可以創建一個相同的無狀態模型,將有狀態模型的參數復制到里面。 創建了一個字符層面的模型,接下來看看詞層面的模型,并做一個常見的自然語言處理任務:情感分析。我們會學習使用遮掩來處理變化長度的序列。 ## 情感分析 如果說 MNIST 是計算機視覺的“hello world”,那么 IMDb 影評數據集就是自然語言處理的“hello world”:這個數據集包含 50000 條英文影評,25000 條用于訓練,25000 條用于測試,是從 IMDb 網站提取的,并帶有影評標簽,負(0)或正(1)。和 MNIST 一樣,IMDb 影評數據集的流行是有原因的:筆記本電腦上就可以跑起來,不會耗時太長,也具有一定挑戰。Keras 提供了一個簡單的函數加載數據集: ```py >>> (X_train, y_train), (X_test, y_test) = keras.datasets.imdb.load_data() >>> X_train[0][:10] [1, 14, 22, 16, 43, 530, 973, 1622, 1385, 65] ``` 影評在哪里?可以看到,數據集已經經過預處理了:`X_train`包括列表形式的影評,每條都是整數 NumPy 數組,每個整數代表一個詞。所有標點符號都被去掉了,單詞轉換為小寫,用空格隔開,最后用頻次建立索引(小整數對應常見詞)。整數 0、1、2 是特殊的:它們表示填充 token、序列開始(SSS)token、和未知單詞。如果想看到影評,可以如下解碼: ```py >>> word_index = keras.datasets.imdb.get_word_index() >>> id_to_word = {id_ + 3: word for word, id_ in word_index.items()} >>> for id_, token in enumerate(("<pad>", "<sos>", "<unk>")): ... id_to_word[id_] = token ... >>> " ".join([id_to_word[id_] for id_ in X_train[0][:10]]) '<sos> this film was just brilliant casting location scenery story' ``` 在真實的項目中,必須要自己預處理文本。你可以使用前面用過的`Tokenizer`,但要設置`char_level=False`(其實是默認的)。當編碼單詞時,`Tokenizer`會過濾掉許多字符,包括多數標點符號、換行符、制表符(可以通過`filters`參數控制)。最重要的,`Tokenizer`使用空格確定單詞的邊界。這對于英語和其它用空格隔開單詞的語言是行得通的,但并不是所有語言都有空格。中文不使用空格,越南語甚至在單詞里也有空格,德語經常將幾個單詞不用空格連在一起。就算在英語中,空格也不總是 token 文本的最好方法:比如 San Francisco 或#ILoveDeepLearning。 幸好,有更好的方法。Taku Kudo 在[2018 年的一篇論文](https://links.jianshu.com/go?to=https%3A%2F%2Fhoml.info%2Fsubword)中介紹了一種無監督學習方法,在亞詞層面 tokenize 和 detokenize 文本,與所屬語言獨立,空格和其它字符等同處理。使用這種方法,就算模型碰到一個之前沒見過的單詞,模型還是能猜出它的意思。例如,模型在訓練期間沒見過單詞 smartest,但學過 est 詞尾是最的意思,然后就可以推斷 smartest 的意思。Google 的[*SentencePiece*](https://links.jianshu.com/go?to=https%3A%2F%2Fgithub.com%2Fgoogle%2Fsentencepiece)項目提供了開源實現,見 Taku Kudo 和 John Richardson 的[論文](https://links.jianshu.com/go?to=https%3A%2F%2Fhoml.info%2Fsentencepiece)。 另一種方法,是 Rico Sennrich 在更早的[論文](https://links.jianshu.com/go?to=https%3A%2F%2Fhoml.info%2Frarewords)中提出的,探索了其它創建亞單詞編碼的方法(比如,使用字節對編碼)。最后同樣重要的,TensorFlow 團隊在 2019 年提出了[TF.Text](https://links.jianshu.com/go?to=https%3A%2F%2Fhoml.info%2Ftftext)庫,它實現了多種 token 化策略,包括[WordPiece](https://links.jianshu.com/go?to=https%3A%2F%2Fhoml.info%2Fwordpiece)(字節對編碼的變種)。 如果你想將模型部署到移動設備或網頁中,又不想每次都寫一個不同的預處理函數,最好只使用 TensorFlow 運算,它可以融進模型中。看看怎么做。首先,使用 TensorFlow Datasets 加載原始 IMDb 評論,為文本(字節串): ```py import tensorflow_datasets as tfds datasets, info = tfds.load("imdb_reviews", as_supervised=True, with_info=True) train_size = info.splits["train"].num_examples ``` 然后,寫預處理函數: ```py def preprocess(X_batch, y_batch): X_batch = tf.strings.substr(X_batch, 0, 300) X_batch = tf.strings.regex_replace(X_batch, b"<br\\s*/?>", b" ") X_batch = tf.strings.regex_replace(X_batch, b"[^a-zA-Z']", b" ") X_batch = tf.strings.split(X_batch) return X_batch.to_tensor(default_value=b"<pad>"), y_batch ``` 預處理函數先裁剪影評,只保留前 300 個字符:這么做可以加速訓練,并且不會過多影響性能,因為大多數時候只要看前一兩句話,就能判斷是正面或側面的了。然后使用正則表達式替換`<br />`標簽為空格,然后將所有非字母字符替換為空格。例如,文本`"Well, I can't<br />"`變成`"Well I can't"`。最后,`preprocess()`函數用空格分隔影評,返回一個嵌套張量,然后將嵌套張量轉變為緊密張量,給所有影評填充上`"<pad>"`,使其長度相等。 然后,構建詞典。這需要使用`preprocess()`函數再次處理訓練集,并使用`Counter`統計每個單詞的出現次數: ```py from collections import Counter vocabulary = Counter() for X_batch, y_batch in datasets["train"].batch(32).map(preprocess): for review in X_batch: vocabulary.update(list(review.numpy())) ``` 看看最常見的詞有哪些: ```py >>> vocabulary.most_common()[:3] [(b'<pad>', 215797), (b'the', 61137), (b'a', 38564)] ``` 但是,并不需要讓模型知道詞典中的所有詞,所以裁剪詞典,只保留 10000 個最常見的詞: ```py vocab_size = 10000 truncated_vocabulary = [ word for word, count in vocabulary.most_common()[:vocab_size]] ``` 現在需要加上預處理步驟將每個單詞替換為單詞 ID(即它在詞典中的索引)。就像第 13 章那樣,創建一張查找表,使用 1000 個未登錄詞(oov)桶: ```py words = tf.constant(truncated_vocabulary) word_ids = tf.range(len(truncated_vocabulary), dtype=tf.int64) vocab_init = tf.lookup.KeyValueTensorInitializer(words, word_ids) num_oov_buckets = 1000 table = tf.lookup.StaticVocabularyTable(vocab_init, num_oov_buckets) ``` 用這個詞表查找幾個單詞的 ID: ```py >>> table.lookup(tf.constant([b"This movie was faaaaaantastic".split()])) <tf.Tensor: [...], dtype=int64, numpy=array([[ 22, 12, 11, 10054]])> ``` 因為 this、movie、was 是在詞表中的,所以它們的 ID 小于 10000,而 faaaaaantastic 不在詞表中,所以將其映射到一個 oov 桶,其 ID 大于或等于 10000。 > 提示:TF Transform 提供了一些實用的函數來處理詞典。例如,`tft.compute_and_apply_vocabulary()`函數:它可以遍歷數據集,找到所有不同的詞,創建詞典,還能生成 TF 運算,利用詞典編碼每個單詞。 現在,可以創建最終的訓練集。對影評做批次,使用`preprocess()`將其轉換為詞的短序列,然后使用一個簡單的`encode_words()`函數,利用創建的詞表來編碼這些詞,最后預提取下一個批次: ```py def encode_words(X_batch, y_batch): return table.lookup(X_batch), y_batch train_set = datasets["train"].batch(32).map(preprocess) train_set = train_set.map(encode_words).prefetch(1) ``` 最后,創建模型并訓練: ```py embed_size = 128 model = keras.models.Sequential([ keras.layers.Embedding(vocab_size + num_oov_buckets, embed_size, input_shape=[None]), keras.layers.GRU(128, return_sequences=True), keras.layers.GRU(128), keras.layers.Dense(1, activation="sigmoid") ]) model.compile(loss="binary_crossentropy", optimizer="Adam", metrics=["accuracy"]) history = model.fit(train_set, epochs=5) ``` 第一個層是一個嵌入層,它將所有單詞 ID 變為嵌入。每有一個單詞 ID(`vocab_size + num_oov_buckets`),嵌入矩陣就有一行,每有一個嵌入維度,嵌入矩陣就有一列(這個例子使用了 128 個維度,這是一個可調的超參數)。模型輸入是 2D 張量,形狀為 [批次大小, 時間步] ,嵌入層的輸出是一個 3D 張量,形狀為 [批次大小, 時間步, 嵌入大小] 。 模型剩下的部分就很簡單了:有兩個`GRU`層,第二個只返回最后時間步的輸出。輸出層只有一個神經元,使用 sigmoid 激活函數,輸出評論是正或負的概率。然后編譯模型,利用前面準備的數據集來訓練幾個周期。 ### 遮掩 在訓練過程中,模型會學習到填充 token 要被忽略掉。但這其實是已知的。為什么不告訴模型直接忽略填充 token,將精力集中在真正重要的數據中呢?只需一步就好:創建嵌入層時加上`mask_zero=True`。這意味著填充 token(其 ID 為 0)可以被接下來的所有層忽略。 其中的原理,是嵌入層創建了一個等于`K.not_equal(inputs, 0)`(其中`K = keras.backend`)遮掩張量:這是一個布爾張量,形狀和輸入相同,只要詞 ID 有 0,它就等于`False`,否則為`True`。模型自動將這個遮掩張量向前傳遞給所有層,只要時間維度保留著。所以在例子中,盡管兩個`GRU`都接收到了遮掩張量,但第二個`GRU`層不返回序列(只返回最后一個時間步),遮掩張量不會傳遞到緊密層。每個層處理遮掩的方式不同,但通常會忽略被遮掩的時間步(遮掩為`False`的時間步)。例如,當循環神經層碰到被遮掩的時間步時,就只是從前一時間步復制輸出而已。如果遮掩張量一直傳遞到輸出(輸出為序列的模型),則遮掩也會作用到損失上,所以遮掩時間步不會貢獻到損失上(它們的損失為 0)。 > 警告:基于英偉達的 cuDNN 庫,`LSTM`層和`GRU`層針對 GPU 有優化實現。但是,這個實現不支持遮擋。如果你的模型使用了遮擋,則這些曾會回滾到(更慢的)默認實現。注意優化實現還需要使用幾個超參數的默認值:`activation`、`recurrent_activation`、`recurrent_dropout`、`unroll`、`use_bias`、`reset_after`。 所有接收遮擋的層必須支持遮擋(否則會拋出異常)。包括所有的循環層、`TimeDistributed`層和其它層。所有支持遮擋的層必須有等于`True`的屬性`supports_masking`。如果想實現自定義的支持遮擋的層,應該給`call()`方法添加`mask`參數。另外,要在構造器中設定`self.supports_masking = True`。如果第一個層不是嵌入層,可以使用`keras.layers.Masking`層:它設置遮擋為`K.any(K.not_equal(inputs, 0), axis=-1)`,意思是最后一維都是 0 的時間步,會被后續層遮擋。 對于`Sequential`模型,使用遮擋層,并自動向前傳遞遮擋是最佳的。但復雜模型上不能這么做,比如將`Conv1D`層與循環層混合使用時。對這種情況,需要使用 Functional API 或 Subclassing API 顯式計算遮擋張量,然后將其傳給需要的層。例如,下面的模型等價于前一個模型,除了使用 Functional API 手動處理遮擋張量: ```py K = keras.backend inputs = keras.layers.Input(shape=[None]) mask = keras.layers.Lambda(lambda inputs: K.not_equal(inputs, 0))(inputs) z = keras.layers.Embedding(vocab_size + num_oov_buckets, embed_size)(inputs) z = keras.layers.GRU(128, return_sequences=True)(z, mask=mask) z = keras.layers.GRU(128)(z, mask=mask) outputs = keras.layers.Dense(1, activation="sigmoid")(z) model = keras.Model(inputs=[inputs], outputs=[outputs]) ``` 訓練幾個周期之后,這個模型的表現就相當不錯了。如果使用`TensorBoard()`調回,可以可視化 TensorBoard 中的嵌入是怎么學習的:可以看到 awesome 和 amazing 這樣的詞漸漸聚集于嵌入空間的一邊,而 awful、terrible 這樣的詞聚集到另一邊。一些詞可能不會像預期那樣是正面的,比如 good,可能所有負面評論含有 not good。模型只基于 25000 個詞就能學會詞嵌入,讓人印象深刻。如果訓練集有幾十億的規模,效果就更好了。但可惜沒有,但可以利用在其它大語料(比如,維基百科文章)上訓練的嵌入,就算不是影評也可以?畢竟,amazing 這個詞在哪種語境的意思都差不多。另外,甚至嵌入是在其它任務上訓練的,也可能有益于情感分析:因為 awesome 和 amazing 有相似的意思,即使對于其它任務(比如,預測句子中的下一個詞),它們也傾向于在嵌入空間聚集,所以對情感分析也是有用的。所以看看能否重復利用預訓練好的詞嵌入。 ### 復用預訓練的詞嵌入 在 TensorFlow Hub 上可以非常方便的找到可以復用的預訓練模型組件。這些模型組件被稱為模塊。只需瀏覽[TF Hub 倉庫](https://links.jianshu.com/go?to=https%3A%2F%2Ftfhub.dev%2F),找到需要的模型,復制代碼到自己的項目中就行,模塊可以總動下載下來,包含預訓練權重,到自己的模型中。 例如,在情感分析模型中使用`nnlm-en-dim50`句子嵌入模塊,版本 1: ```py import tensorflow_hub as hub model = keras.Sequential([ hub.KerasLayer("https://tfhub.dev/google/tf2-preview/nnlm-en-dim50/1", dtype=tf.string, input_shape=[], output_shape=[50]), keras.layers.Dense(128, activation="relu"), keras.layers.Dense(1, activation="sigmoid") ]) model.compile(loss="binary_crossentropy", optimizer="Adam", metrics=["accuracy"]) ``` `hub.KerasLayer`從給定的 URL 下載模塊。這個特殊的模塊是“句子編碼器”:它接收字符串作為輸入,將每句話編碼為一個獨立矢量(這個例子中是 50 維度的矢量)。在內部,它將字符串解析(空格分隔),然后使用預訓練(訓練語料是 Google News 7B,一共有 70 億個詞)的嵌入矩陣來嵌入每個詞。然后計算所有詞嵌入的平均值,結果是句子嵌入。我們接著可以添加兩個簡單的緊密層來創建一個出色的情感分析模型。默認,`hub.KerasLayer`是不可訓練的,但創建時可以設定`trainable=True`,就可以針對自己的任務微調了。 > 警告:不是所有的 TF Hub 模塊都支持 TensorFlow 2。 然后,就可以加載 IMDb 影評數據集了,不需要預處理(但要做批次和預提取),直接訓練模型就成: ```py datasets, info = tfds.load("imdb_reviews", as_supervised=True, with_info=True) train_size = info.splits["train"].num_examples batch_size = 32 train_set = datasets["train"].batch(batch_size).prefetch(1) history = model.fit(train_set, epochs=5) ``` 注意到,TF Hub 模塊的 URL 的末尾指定了是模型的版本 1。版本號可以保證當有新的模型版本發布時,不會破壞自己的模型。如果在瀏覽器中輸入這個 URL,能看到這個模塊的文檔。TF Hub 會默認將下載文件緩存到系統的臨時目錄。你可能想將文件存儲到固定目錄,以免每次系統清洗后都要下載。要這么做的話,設置環境變量`TFHUB_CACHE_DIR`就成(比如,`os.environ["TFHUB_CACHE_DIR"] = "./my_tfhub_cache"`)。 截至目前,我們學習了時間序列、用 Char-RNN 生成文本、用 RNN 做情感分析、訓練自己的詞嵌入或復用預訓練詞嵌入。接下來看看另一個重要的 NLP 任務:神經網絡機器翻譯(NMT),我們先使用純粹的編碼器-解碼器模型,然后使用注意力機制,最后看看 Transformer 架構。 ## 用編碼器-解碼器做機器翻譯 看一個簡單的[神經網絡機器翻譯模型](https://links.jianshu.com/go?to=https%3A%2F%2Fhoml.info%2F103),它能將英語翻譯為法語(見圖 16-3)。 簡而言之,英語句子輸入進編碼器,解碼器輸出法語。注意,法語翻譯也作為解碼器的輸入,但向后退一步。換句話說,解碼器將前一步的輸出再作為輸入(不用管它輸出什么)。對于第一個詞,給它加上一個序列開始(SOS)token,序列結尾加上序列結束(EOS)token。 英語句子在輸入給編碼器之前,先做了翻轉。例如,“I drink milk”翻轉為“milk drink I”。這樣能保證英語句子的第一個詞是最后一個輸入給編碼器的,通常也是解碼器要翻譯的第一個詞。 每個單詞首先用它的 ID 來表示(例如,288 代表 milk)。然后,嵌入層返回單詞嵌入。單詞嵌入才是輸入給編碼器和解碼器的。 ![](https://img.kancloud.cn/e9/47/e94726ac2e45d5d470cb3f96d2efc6e7_1441x974.png)圖 16-3 一個簡單的機器翻譯模型 在每一步,解碼器輸出一個輸出詞典中每個單詞的分數,然后 softmax 層將分數變為概率。例如,在第一步,“Je”的概率可能為 20%,“Tu”的概率可能為 1%,等等。概率最高的詞作為輸出。這特別像一個常規分類任務,所以可以用`"sparse_categorical_crossentropy"`損失訓練模型,跟前面的 Char-RNN 差不多。 在做推斷時,沒有目標語句輸入進解碼器。相反的,只是輸入解碼器前一步的輸出,見圖 16-4(這需要一個嵌入查找表,圖中沒有展示)。 ![](https://img.kancloud.cn/d7/f8/d7f8dc5ae77d1e6b1f628ca3ffd79096_1223x552.png)圖 16-4 在推斷時,將前一步的輸出作為輸入 好了,現在知道整體的大概了。但要實現模型的話,還有幾個細節要處理: * 目前假定所有(編碼器和解碼器的)輸入序列的長度固定。但很顯然句子長度是變化的。因為常規張量的形狀固定,它們只含有相同長度的句子。可以用遮擋來處理;但如果句子的長度非常不同,就不能像之前情感分析那樣截斷(因為想要的是完整句子的翻譯)。可以將句子放進長度相近的桶里(一個桶放 1 個詞到 6 個詞的句子,一個桶放 7 個詞到 12 個詞的句子,等等),給短句子加填充,使同一個桶中的句子長度相同(見`tf.data.experimental.bucket_by_sequence_length()`函數)。例如,“I drink milk” 變為 “<pad> <pad> <pad> milk drink I”。 * 要忽略所有在 EOS token 后面的輸出,這些輸出不能影響損失(遮擋起來)。例如,如果模型輸出“Je bois du lait <eos> oui”,忽略最后一個詞對損失的影響。 * 如果輸出詞典比較大(這個例子就是這樣),輸出每個詞的概率會非常慢。如果目標詞典有 50000 個發語詞,則解碼器要輸出 50000 維的矢量,在這個矢量上計算 softmax 非常耗時。一個方法是只查看模型對正確詞和非正確詞采樣的對數概率輸出,然后根據這些對數概率計算一個大概的損失。這個采樣 softmax 方法是[Sébastien Jean 在 2015 年提出的](https://links.jianshu.com/go?to=https%3A%2F%2Fhoml.info%2F104)。在 TensorFlow 中,你可以在訓練時使用`tf.nn.sampled_softmax_loss()`,在推斷時使用常規 softmax 函數(推斷時不能使用采樣 softmax,因為需要知道目標)。 TensorFlow Addons 項目涵蓋了許多序列到序列的工具,可以創建準生產的編碼器-解碼器。例如,下面的代碼創建了一個基本的編碼器-解碼器模型,相似于圖 16-3: ```py import tensorflow_addons as tfa encoder_inputs = keras.layers.Input(shape=[None], dtype=np.int32) decoder_inputs = keras.layers.Input(shape=[None], dtype=np.int32) sequence_lengths = keras.layers.Input(shape=[], dtype=np.int32) embeddings = keras.layers.Embedding(vocab_size, embed_size) encoder_embeddings = embeddings(encoder_inputs) decoder_embeddings = embeddings(decoder_inputs) encoder = keras.layers.LSTM(512, return_state=True) encoder_outputs, state_h, state_c = encoder(encoder_embeddings) encoder_state = [state_h, state_c] sampler = tfa.seq2seq.sampler.TrainingSampler() decoder_cell = keras.layers.LSTMCell(512) output_layer = keras.layers.Dense(vocab_size) decoder = tfa.seq2seq.basic_decoder.BasicDecoder(decoder_cell, sampler, output_layer=output_layer) final_outputs, final_state, final_sequence_lengths = decoder( decoder_embeddings, initial_state=encoder_state, sequence_length=sequence_lengths) Y_proba = tf.nn.softmax(final_outputs.rnn_output) model = keras.Model(inputs=[encoder_inputs, decoder_inputs, sequence_lengths], outputs=[Y_proba]) ``` 這個代碼很簡單,但有幾點要注意。首先,創建`LSTM`層時,設置`return_state=True`,以便得到最終隱藏態,并將其傳給解碼器。因為使用的是 LSTM 單元,它實際返回兩個隱藏態(短時和長時)。`TrainingSampler`是 TensorFlow Addons 中幾個可用的采樣器之一:它的作用是在每一步告訴解碼器,前一步的輸出是什么。在推斷時,采樣器是實際輸出的 token 嵌入。在訓練時,是前一個目標 token 的嵌入:這就是為什么使用`TrainingSampler`的原因。在實際中,一個好方法是,一開始用目標在前一時間步的嵌入訓練,然后逐漸過渡到實際 token 在前一步的輸出。這個方法是 Samy Bengio 在[2015 年的一篇論文](https://links.jianshu.com/go?to=https%3A%2F%2Fhoml.info%2Fscheduledsampling)中提出的。`ScheduledEmbeddingTrainingSampler`可以隨機從目標或實際輸出挑選,你可以在訓練中逐漸調整概率。 ### 雙向 RNN 在每個時間步,常規循環層在產生輸出前,只會查看過去和當下的輸入。換句話說,循環層是遵循因果關系的,它不能查看未來。這樣的 RNN 在預測時間序列時是合理的,但對于許多 NLP 任務,比如機器翻譯,在編碼給定詞時,最好看看后面的詞是什么。比如,對于這幾個短語“the Queen of the United Kingdom”、“the queen of hearts”、“the queen bee”:要正確編碼“queen”,需要向前看。要實現的話,可以對于相同的輸入運行兩個循環層,一個從左往右讀,一個從右往左讀。然后將每個時間步的輸出結合,通常是連起來。這被稱為雙向循環層(見圖 16-5)。 要在 Keras 中實現雙向循環層,可以在`keras.layers.Bidirectional`層中包一個循環層。例如,下面的代碼創建了一個雙向`GRU`層: ```py keras.layers.Bidirectional(keras.layers.GRU(10, return_sequences=True)) ``` > 筆記:`Bidirectional`層會創建一個`GRU`層的復制(但方向相反),會運行兩個層,并將輸出連起來。因此`GRU`層有 10 個神經元,`Bidirectional`層在每個時間步會輸出 20 個值。 ![](https://img.kancloud.cn/73/c6/73c6f0b94f662b5b82f243395132f181_964x602.png)圖 16-5 雙向循環層 ### 集束搜索 假設你用編碼器-解碼器模型將法語“Comment vas-tu?”翻譯為英語。正確的翻譯應該是“How are you?”,但得到的結果是“How will you?”。查看訓練集,發現許多句子,比如“Comment vas-tu jouer?”翻譯成了“How will you play?”。所以模型看到“Comment vas”之后,將其翻譯為“How will”并不那么荒唐。但在這個例子中,這就是一個錯誤,并且模型還不能返回修改,模型只能盡全力完成句子。如果每步都是最大貪心地輸出結果,只能得到次優解。如何能讓模型返回到之前的錯誤并改錯呢?最常用的方法之一,是使用集束搜索:它跟蹤 k 個最大概率的句子列表,在每個解碼器步驟延長一個詞,然后再關注其中 k 個最大概率的句子。參數 k 被稱為集束寬度。 例如,假設使用寬度為 3 的集束搜索,用模型來翻譯句子“Comment vas-tu?”。在第一個解碼步驟,模型會輸出每個可能詞的估計概率。假設前 3 個詞的估計概率是“How”(估計概率是 75%)、“What”(3%)、“You”(1%)。這是目前的句子列表。然后,創建三個模型的復制,預測每個句子的下一個詞。第一個模型會預測“How”后面的詞,假設結果是 36%為“will”、32%為“are”、16%為“do”,等等。注意,這是條件概率。第二個模型會預測“What”后面的詞:50%為“are”,等等。假設詞典有 10000 個詞,每個模型會輸出 10000 個概率。 然后,計算 30000 個含有兩個詞的句子的概率。將條件概率相乘。例如,“How will”的概率是 75% × 36% = 27%。計算完 30000 個概率之后,只保留概率最大的 3 個。假設是“How will” (27%)、“How are” (24%)、“How do” (12%)。現在“How will”的概率最大,但“How are”并沒有被刪掉。 接著,重復同樣的過程:用三個模型預測這三個句子的接下來的詞,再計算 30000 個含有三個詞的句子的概率。假設前三名是“How are you” (10%)、“How do you” (8%)、“How will you” (2%)。再下一步的前三名是“How do you do” (7%)、“How are you <eos>” (6%)、“How are you doing” (3%)。注意,“How will”被淘汰了。沒有使用額外的訓練,只是在使用層面做了改動,就提高了模型的性能。 TensorFlow Addons 可以很容易實現集束搜索: ```py beam_width = 10 decoder = tfa.seq2seq.beam_search_decoder.BeamSearchDecoder( cell=decoder_cell, beam_width=beam_width, output_layer=output_layer) decoder_initial_state = tfa.seq2seq.beam_search_decoder.tile_batch( encoder_state, multiplier=beam_width) outputs, _, _ = decoder( embedding_decoder, start_tokens=start_tokens, end_token=end_token, initial_state=decoder_initial_state) ``` 首先創建`BeamSearchDecoder`,它包裝所有的解碼器的克隆(這個例子中有 10 個)。然后給每個解碼器克隆創建一個編碼器的最終狀態的復制,然后將狀態傳給解碼器,加上開始和結束 token。 有了這些,就能得到不錯的短句的翻譯了(如果使用預訓練詞嵌入,效果更好)。但是這個模型翻譯長句子的效果很糟。這又是 RNN 的短時記憶問題。注意力機制的出現,解決了這一問題。 ## 注意力機制 圖 16-3 中,從“milk”到“lait”的路徑非常長。這意味著這個單詞的表征(還包括其它詞),在真正使用之前,要經過許多步驟。能讓這個路徑短點嗎? 這是 Dzmitry Bahdanau 在 2014 年的突破性[論文](https://links.jianshu.com/go?to=https%3A%2F%2Fhoml.info%2Fattention)中的核心想法。他們引入了一種方法,可以讓解碼器在每個時間步關注特別的(被編碼器編碼的)詞。例如,在解碼器需要輸出單詞“lait”的時間步,解碼器會將注意力關注在單詞“milk”上。這意味著從輸入詞到其翻譯結果的路徑變的短得多了,所以 RNN 的短時記憶的限制就減輕了很多。注意力機制革新了神經網絡機器翻譯(和 NLP 的常見任務),特別是對于長句子(超過 30 個詞),帶來了非凡的進步。 圖 16-6 展示了注意力機制的架構(稍微簡化過,后面會說明)。左邊是編碼器和解碼器。不是將編碼器的最終隱藏態傳給解碼器(其實是傳了,但圖中沒有展示),而是將所有的輸出傳給解碼器。在每個時間步,解碼器的記憶單元計算所有這些輸出的加權和:這樣可以確定這一步關注哪個詞。權重α<sub>(t,i)</sub>是第 i<sup>th</sup>個編碼器輸出在第 t<sup>th</sup>解碼器時間步的權重。例如,如果權重α<sub>(3,2)</sub>比α<sub>(3,0)</sub>和α<sub>(3,1)</sub>大得多,則解碼器會用更多注意力關注詞 2(“milk”),至少是在這個時間步。剩下的解碼器就和之前一樣工作:在每個時間步,記憶單元接收輸入,加上上一個時間步的隱藏態,最后(這一步圖上沒有畫出)加上上一個時間步的目標詞(或推斷時,上一個時間步的輸出)。 ![](https://img.kancloud.cn/8d/79/8d792ecb6ea76e59df5ed406732dc4d4_1440x910.png)圖 16-6 使用了注意力模型的編碼器-解碼器結構 權重α<sub>(t,i)</sub>是從哪里來的呢?其實很簡單:是用一種小型的、被稱為對齊模型(或注意力層)的神經網絡生成的,注意力層與模型的其余部分聯合訓練。對齊模型展示在圖的右邊:一開始是一個時間分布緊密層,其中有一個神經元,它接收所有編碼器的輸出,加上解碼器的上一個隱藏態(即 h<sub>(2)</sub>)。這個層輸出對每個編碼器輸出,輸出一個分數(或能量)(例如,e<sub>(3, 2)</sub>):這個分數衡量每個輸出和解碼器上一個隱藏態的對齊程度。最后,所有分數經過一個 softmax 層,得到每個編碼器輸出的最終權重(例如,α<sub>(3, 2)</sub>)。給定解碼器時間步的所有權重相加等于 1(因為 softmax 層不是時間分布的)。這個注意力機制稱為 Bahdanau 注意力。因為它將編碼器輸出和解碼器的上一隱藏態連了起來,也被稱為連接注意力(或相加注意力)。 > 筆記:如果輸入句子有 n 個單詞,假設輸出也是這么多單詞,則要計算 n<sup>2</sup>個權重。幸好,平方計算的復雜度不高,因為即使是特別長的句子,也不會有數千個單詞。 另一個常見的注意力機制是不久之后,由 Minh-Thang Luong 在 2015 年的[論文](https://links.jianshu.com/go?to=https%3A%2F%2Fhoml.info%2Fluongattention)中提出的。因為注意力機制的目標是衡量編碼器的輸出,和解碼器上一隱藏態的相似度,Minh-Thang Luong 提出,只要計算這兩個矢量的點積,因為點積是有效衡量相似度的手段,并且計算起來很快。要計算的話,兩個矢量的維度必須相同。這被稱為 Luong 注意力,或相乘注意力。和 Bahdanau 注意力一樣,點積的結果是一個分數,所有分數(在特定的解碼器時間步)通過 softmax 層,得到最終權重。Luong 提出的另一個簡化方法是使用解碼器在當前時間步的隱藏態,而不是上一時間步,然后使用注意力機制的輸出(標記為![\hat{h}](https://img.kancloud.cn/b9/e5/b9e5017c6506bac185c51b3da8bff414_8x17.png)<sub>(t)</sub> ),直接計算解碼器的預測(而不是計算解碼器的當前隱藏態)。他還提出了一個點擊的變體,編碼器的輸出先做線性變換(即,時間分布緊密層不加偏置項),再做點積。這被稱為“通用”點積方法。作者比較了點積方盒和連接注意力機制(加上一個縮放參數 v),觀察到點積方法的變體表現的更好。因為這個原因,如今連接注意力很少使用了。公式 16-1 總結了這三種注意力機制。 ![](https://img.kancloud.cn/ca/bc/cabc6ca73a8a1452f040172bb89e9649_1184x656.png)公式 16-1 注意力機制 使用 TensorFlow Addons 將 Luong 注意力添加到編碼器-解碼器模型的方法如下: ```py attention_mechanism = tfa.seq2seq.attention_wrapper.LuongAttention( units, encoder_state, memory_sequence_length=encoder_sequence_length) attention_decoder_cell = tfa.seq2seq.attention_wrapper.AttentionWrapper( decoder_cell, attention_mechanism, attention_layer_size=n_units) ``` 只是將解碼器單元包裝進`AttentionWrapper`,然后使用了想用的注意力機制(這里用的是 Luong 注意力)。 ### 視覺注意力 注意力機制如今應用的非常廣泛。最先用途之一是利用視覺注意力生成圖片標題:卷積神經網絡首先處理圖片,生成一些特征映射,然后用帶有注意力機制的解碼器 RNN 來生成標題,每次生成一個詞。在每個解碼器時間步(每個詞),解碼器使用注意力模型聚焦于圖片的一部分。例如,對于圖 16-7,模型生成的標題是“一個女人正在公園里扔飛盤”,可以看到解碼器要輸出單詞“飛盤”時,注意力關注的圖片的部分:顯然,注意力大部分聚焦于飛盤。 ![](https://img.kancloud.cn/ec/8d/ec8daf5f9f79fe123b962695df89d635_1439x689.png)圖 16-7 視覺注意力:輸入圖片(左)和模型輸出“飛盤”時模型的關注點(右) > 解釋性 > > 注意力機制的的一個額外的優點,是它更容易使人明白是什么讓模型產生輸出。這被稱為可解釋性。當模型犯錯時,可解釋性非常有幫助:例如,如果一張狗在雪中行走的圖,被打上了“狼在雪中行走”的標簽,你就可以回去查看當模型輸出“狼”時,模型聚焦于什么。你可能看到模型不僅關注于狗,還關注于雪地,暗示了一種可能的解釋:可能模型判斷是根據有沒有很多雪,來判斷是狗還是狼。然后可以通過用更多沒有雪的狼的圖片進行訓練,來修復模型。這個例子來自于 Marco Tulio Ribeiro 在 2016 年的[論文](https://links.jianshu.com/go?to=https%3A%2F%2Fhoml.info%2Fexplainclass),他們使用了不同的可解釋性:局部圍繞分類器的預測,來學習解釋性模型。 > > 在一些應用中,可解釋性不僅是調試模型的工具,而是正當的需求(比如一個判斷是否進行放貸的需求)。 注意力機制如此強大,以至于只需要注意力機制就能創建出色的模型。 ### Attention Is All You Need:Transformer 架構 在 2017 年一篇突破性[論文](https://links.jianshu.com/go?to=https%3A%2F%2Fhoml.info%2Ftransformer)中,谷歌的研究者提出了:Attention Is All You Need(只要注意力)。他們創建了一種被稱為 Transformer(轉換器)的架構,它極大的提升了 NMT 的性能,并且沒有使用任何循環或卷積層,只用了注意力機制(加上嵌入層、緊密層、歸一化層,和一些其它組件)。這個架構的另一個優點,是訓練的更快,且更容易并行運行,花費的時間和精力比之前的模型少得多。 Transformer 架構見圖 16-8。 ![](https://img.kancloud.cn/42/29/42296fe386bb4eb4d08afed67627a7e8_1440x2119.png)圖 16-8 Transformer 架構 一起看下這個架構: * 圖的左邊和以前一樣是編碼器,接收的輸入是一個批次的句子,表征為序列化的單詞 ID(輸入的形狀是 [批次大小, 最大輸入句子長度] ),每個單詞表征為 512 維(所以編碼器的輸出形狀是 [批次大小, 最大輸入句子長度, 512] )。注意,編碼器的頭部疊加了 N 次(論文中,N=6)。 * 架構的右邊是解碼器。在訓練中,它接收目標句子作為輸入(也是表征為序列化的單詞 ID),向右偏移一個時間步(即,在起點插入一個 SOS token)。它還接收編碼器的輸出(即,來自左邊的箭頭)。注意,解碼器的頭部也重疊了 N 次,編碼器的最終輸出,傳入給解碼器重疊層中的每一個部分。和以前一樣,在每個時間步,解碼器輸出每個下一個可能詞的概率(輸出形狀是 [批次大小, 最大輸出句子長度, 詞典長度] )。 * 在推斷時,解碼器不能接收目標,所以輸入的是前面的輸出詞(起點用 SOS token)。因此模型需要重復被調用,每一輪預測一個詞(預測出來的詞在下一輪輸入給解碼器,直到輸出 EOS token)。 * 仔細觀察下,可以看到其實你已經熟悉其中大部分組件了:兩個嵌入層,5 × N 個跳連接,每個后面是一個歸一化層,2 × N 個 “Feed Forward” 模塊(由兩個緊密層組成(第一個使用 ReLU 激活函數,第二個不使用激活函數),輸出層是使用 softmax 激活函數的緊密層)。所有這些層都是時間分布的,因此每個詞是獨立處理的。但是一次只看一個詞,該如何翻譯句子呢?這時就要用到新組件了: * 編碼器的多頭注意力層,編碼每個詞與句子中其它詞的關系,對更相關的詞付出更多注意力。例如,輸出句子“They welcomed the Queen of the United Kingdom”中的詞“Queen”的層的輸出,會取決于句子中的所有詞,但更多注意力會在“United”和“Kingdom”上。這個注意力機制被稱為自注意力(句子對自身注意)。后面會討論它的原理。解碼器的遮擋多頭注意力層做的事情一樣,但每個詞只關注它前面的詞。最后,解碼器的上層多頭注意力層,是解碼器用于在輸入句子上付出注意力的。例如,當解碼器要輸出“Queen”的翻譯時,解碼器會對輸入句子中的“Queen”這個詞注意更多。 * 位置嵌入是緊密矢量(類似詞嵌入),表示詞在句子中的位置。第 n<sup>th</sup>個位置嵌入,添加到每個句子中的第 n<sup>th</sup>個詞上。這可以讓模型知道每個詞的位置,這是因為多頭注意力層不考慮詞的順序或位置,它只看關系。因為所有其它層都是時間分布的,它們不知道每個詞的(相對或絕對)位置。顯然,相對或絕對的詞的位置非常重要,因此需要將位置信息以某種方式告訴 Transformer,位置嵌入是行之有效的方法。 下面逐一仔細介紹 Transformer 中的新組件,從位置嵌入開始。 ### 位置嵌入 位置嵌入是一個緊密矢量,它對詞在句子中的位置進行編碼:第 i<sup>th</sup>個位置嵌入添加到句子中的第 i<sup>th</sup>個詞。模型可以學習這些位置嵌入,但在論文中,作者傾向使用固定位置嵌入,用不同頻率的正弦和余弦函數來定義。公式 16-2 定義了位置嵌入矩陣 P,見圖 16-9 的底部(做過轉置),其中 P<sub>p,i</sub>是單詞在句子的第 p<sup>th</sup>個位置的第 i<sup>th</sup>個嵌入的組件。 ![](https://img.kancloud.cn/35/38/3538c67bf4230bc94652a4622abc7cdf_746x260.png)公式 16-2 正弦/余弦位置嵌入![](https://img.kancloud.cn/4a/dd/4addad8402888567257852d9b5a8c756_1441x784.png)圖 16-9 正弦/余弦位置嵌入矩陣(經過轉置,上),關注 i 的兩個值(下) 這個方法的效果和學習過的位置嵌入相同,但可以拓展到任意長度的句子上,這是它受歡迎的原因。給詞嵌入加上位置嵌入之后,模型剩下的部分就可以訪問每個詞在句子中的絕對位置了,因為每個值都有一個獨立的位置嵌入(比如,句子中第 22nd 個位置的詞的位置嵌入,表示為圖 16-9 中的左下方的垂直虛線,可以看到位置嵌入對這個位置是一對一的)。另外,振動函數(正弦和余弦)選擇也可以讓模型學到相對位置。例如,相隔 38 個位置的詞(例如,在位置 p=22 和 p=60)總是在嵌入維度 i=100 和 i=101 有相同的位置嵌入值,見圖 16-9。這解釋了對于每個頻率,為什么需要正弦和余弦兩個函數:如果只使用正弦(藍線, i=100),模型不能區分位置 p=25 和 p=35 (叉子標記)。 TensorFlow 中沒有`PositionalEmbedding`層,但創建很容易。出于效率的考量,在構造器中先計算出位置嵌入(因此需要知道最大句子長度,`max_steps`,每個詞表征的維度,`max_dims`)。然后調用`call()`方法裁剪嵌入矩陣,變成輸入的大小,然后添加到輸入上。因為創建位置嵌入矩陣時,添加了一個大小為 1 的維度,廣播機制可以確保位置矩陣添加到輸入中的每個句子上: ```py class PositionalEncoding(keras.layers.Layer): def __init__(self, max_steps, max_dims, dtype=tf.float32, **kwargs): super().__init__(dtype=dtype, **kwargs) if max_dims % 2 == 1: max_dims += 1 # max_dims must be even p, i = np.meshgrid(np.arange(max_steps), np.arange(max_dims // 2)) pos_emb = np.empty((1, max_steps, max_dims)) pos_emb[0, :, ::2] = np.sin(p / 10000**(2 * i / max_dims)).T pos_emb[0, :, 1::2] = np.cos(p / 10000**(2 * i / max_dims)).T self.positional_embedding = tf.constant(pos_emb.astype(self.dtype)) def call(self, inputs): shape = tf.shape(inputs) return inputs + self.positional_embedding[:, :shape[-2], :shape[-1]] ``` 然后可以創建 Transformer 的前幾層: ```py embed_size = 512; max_steps = 500; vocab_size = 10000 encoder_inputs = keras.layers.Input(shape=[None], dtype=np.int32) decoder_inputs = keras.layers.Input(shape=[None], dtype=np.int32) embeddings = keras.layers.Embedding(vocab_size, embed_size) encoder_embeddings = embeddings(encoder_inputs) decoder_embeddings = embeddings(decoder_inputs) positional_encoding = PositionalEncoding(max_steps, max_dims=embed_size) encoder_in = positional_encoding(encoder_embeddings) decoder_in = positional_encoding(decoder_embeddings) ``` 接下來看看 Transformer 的核心:多頭注意力層。 ### 多頭注意力 要搞懂多頭注意力層的原理,必須先搞懂收縮點積注意力層(Scaled Dot-Product Attention),多頭注意力是基于它的。假設編碼器分析輸入句子“They played chess”,編碼器分析出“They”是主語,“played”是動詞,然后用詞的表征編碼這些信息。假設解碼器已經翻譯了主語,接下來要翻譯動詞。要這么做的話,它需要從輸入句子取動詞。這有點像查詢字典:編碼器創建了字典{“subject”: “They”, “verb”: “played”, …},解碼器想查找鍵“verb”對應的值是什么。但是,模型沒有離散的 token 來表示鍵(比如“subject” 或 “verb”);它只有這些(訓練中學到的)信息的矢量化表征所以用來查詢的鍵,不會完美對應前面字典中的鍵。解決的方法是計算查詢詞和鍵的相似度,然后用 softmax 函數計算概率權重。如果表示動詞的鍵和查詢詞很相似,則鍵的權重會接近于 1。然后模型可以計算對應值的加權和,如果“verb”鍵的權重接近 1,則加權和會接近于詞“played”的表征。總而言之,可以將整個過程當做字典查詢。Transformer 使用點積做相似度計算,和 Luong 注意力一樣。實際上,公式和 Luong 注意力一樣,除了有縮放參數,見公式 16-3,是矢量的形式。 ![](https://img.kancloud.cn/8e/dc/8edcbe37bef7cac98414831f324e3ebb_1124x194.png)公式 16-3 縮放點積注意力 在這個公式中: * Q 矩陣每行是一個查詢詞。它的形狀是[n<sub>queries</sub>, d<sub>keys</sub>],n<sub>queries</sub>是查詢數,d<sub>keys</sub>是每次查詢和每個鍵的維度數。 * K 矩陣每行是一個鍵。它的形狀是 [n<sub>keys</sub>, d<sub>keys</sub>],n<sub>keys</sub>是鍵和值的數量。 * V 矩陣每行是一個值。它的形狀是 [n<sub>keys</sub>, d<sub>values</sub>],d<sub>values</sub>是每個值的數。 * Q K<sup>T</sup>的形狀是 [n<sub>queries</sub>, n<sub>keys</sub>]:它包含這每個查詢/鍵對的相似分數。softmax 函數的輸出有相同的形狀,且所有行的和是 1。最終的輸出形狀是[n<sub>queries</sub>, d<sub>values</sub>] ,每行代表一個查詢結果(值的加權和)。 * 縮放因子縮小了相似度分數,防止 softmax 函數飽和(飽和會導致梯度變小)。 * 在計算 softmax 之前,通過添加一些非常大的負值,到對應的相似度分上,可以遮擋一些鍵值對。這在遮擋多頭機制層中很有用。 在編碼器中,這個公式應用到批次中的每個句子,Q、K、V 等于輸入句中的詞列表(所以,句子中的每個詞會和相同句中的每個詞比較,包括自身)。相似的,在解碼器的遮擋注意力層中,這個公式會應用到批次中每個目標句上,但要用遮擋,防止每個詞和后面的詞比較(因為在推斷時,解碼器只能訪問已經輸出的詞,所以訓練時要遮擋后面的輸出 token)。在解碼器的上邊的注意力層,鍵 K 矩陣和值 V 矩陣是斌嗎器生成的此列表,查詢 Q 矩陣是解碼器生成的詞列表。 `keras.layers.Attention`層實現了縮放點積注意力,它的輸入是 Q、K、V,除此之外,還有一個批次維度(第一個維度)。 > 提示:在 TensorFlow 中,如果 A 和 B 是兩個維度大于 2 的張量 —— 比如,分別是 [2, 3, 4, 5] 和 [2, 3, 5, 6] —— 則`then tf.matmul(A, B)`會將這兩個張量當做 2 × 3 的數組,每個單元都是一個矩陣,它會乘以對應的矩陣。A 中第 i 行、第 j 列的矩陣,會乘以 B 的第 i 行、第 j 列的矩陣。因為 4 × 5 矩陣乘以 5 × 6 矩陣,結果是 4 × 6 矩陣,所以`tf.matmul(A, B)`的結果數組的形狀是[2, 3, 4, 6]。 如果忽略跳連接、歸一化層、Feed Forward 塊,且這是縮放點積注意力,不是多頭注意力,則 Transformer 可以如下實現: ```py Z = encoder_in for N in range(6): Z = keras.layers.Attention(use_scale=True)([Z, Z]) encoder_outputs = Z Z = decoder_in for N in range(6): Z = keras.layers.Attention(use_scale=True, causal=True)([Z, Z]) Z = keras.layers.Attention(use_scale=True)([Z, encoder_outputs]) outputs = keras.layers.TimeDistributed( keras.layers.Dense(vocab_size, activation="softmax"))(Z) ``` `use_scale=True`參數可以讓層學會如何縮小相似度分數。這是和 Transformer 的一個區別,后者總是用相同的因子()縮小相似度分數。`causal=True`參數,可以讓注意力層的每個輸出 token 只注意前面的輸出 token。 下面來看看多頭注意力層是什么?它的架構見圖 16-10。 ![](https://img.kancloud.cn/a0/1d/a01d5917d99fefe7ef6d3ae0d17e71da_829x1110.png)圖 16-10 多頭注意力層架構 可以看到,它包括一組縮放點積注意力層,每個前面有一個值、鍵、查詢的線性變換(即,時間分布緊密層,沒有激活函數)。所有輸出簡單連接起來,再通過一個最終的線性變換。為什么這么做?這個架構的背后意圖是什么?考慮前面討論過的單詞“played”。編碼器可以將它是動詞的信息做編碼。同時,詞表征還包含它在文本中的位置(得益于位置嵌入),除此之外,可能還包括了其它有用的信息,比如時態。總之,詞表征編碼了詞的許多特性。如果只用一個縮放點積注意力層,則只有一次機會來查詢所有這些特性。這就是為什么多頭注意力層使用了多個不同的值、鍵、查詢的線性變換:這可以讓模型將詞表征投影到不同的亞空間,每個關注于詞特性的一個子集。也許一個線性層將詞表征投影到一個亞空間,其中的信息是該詞是個動詞,另一個線性層會提取它是一個過去式,等等。然后縮放點積注意力做查詢操作,最后將所有結果串起來,在投射到原始空間。 在寫作本書時,TensorFlow 2 還沒有`Transformer`類或`MultiHeadAttention`類。但是,可以查看 TensorFlow 的這個教程:[創建語言理解的 Transformer 模型](https://links.jianshu.com/go?to=https%3A%2F%2Fhoml.info%2Ftransformertuto)。另外,TF Hub 團隊正向 TensorFlow 2 移植基于 Transformer 的模塊,很快就可以用了。同時,我希望我向你展示了自己實現 Transformer 并不難,這是一個很好的練習! ## 語言模型的最新進展 2018 年被稱為“NLP 的 ImageNet 時刻”:成果驚人,產生了越來越大的基于 LSTM 和 Transformer、且在大數據集上訓練過的架構。建議你看看下面的論文,都是 2018 年發表的: * Matthew Peters 的[ELMo 論文](https://links.jianshu.com/go?to=https%3A%2F%2Fhoml.info%2Felmo),介紹了語言模型的嵌入(Embeddings from Language Models (ELMo)):學習深度雙向語言模型的內部狀態,得到的上下文詞嵌入。例如,詞“queen”在“Queen of the United Kingdom”和“queen bee”中的嵌入不同。 * Jeremy Howard 和 Sebastian Ruder 的[ULMFiT 論文](https://links.jianshu.com/go?to=https%3A%2F%2Fhoml.info%2Fulmfit),介紹了無監督預訓練對 NLP 的有效性:作者用海量語料,使用自監督學習(即,從數據自動生成標簽)訓練了一個 LSTM 語言模型,然后在各種任務上微調模型。他們的模型在六個文本分類任務上取得了優異的結果(將誤差率降低了 18-24%)。另外,他們證明,通過在 100 個標簽樣本上微調預訓練模型,可以達到在 10000 個樣本上訓練的效果。 * Alec Radford 和其他 OpenAI 人員的[GPT 論文](https://links.jianshu.com/go?to=https%3A%2F%2Fhoml.info%2Fgpt),也展示了無監督訓練的有效性,但他們使用的是類似 Transformer 的架構。作者預訓練了一個龐大但簡單的架構,由 12 個 Transformer 模塊組成(只使用了遮擋多頭注意力機制),也是用自監督訓練的。然后在多個語言任務上微調,只對每個任務做了小調整。任務種類很雜:包括文本分類、銜接(句子 A 是否跟著句子 B),相似度(例如,“Nice weather today”和“It is sunny”很像),還有問答(通過閱讀幾段文字,讓模型來回答多選題)。幾個月之后,在 2019 年的二月,Alec Radford、Jeffrey Wu 和其它 OpenAI 的人員發表了[GPT-2 論文](https://links.jianshu.com/go?to=https%3A%2F%2Fhoml.info%2Fgpt2),介紹了一個相似的架構,但是更大(超過 15 億參數),他們展示了這個架構可以在多個任務上取得優異的表現,且不需要微調。這被稱為零次學習(zero-shot learning (ZSL))。[*https://github.com/openai/gpt-2*](https://links.jianshu.com/go?to=https%3A%2F%2Fgithub.com%2Fopenai%2Fgpt-2)上是一個 GPT-2 模型的帶有預訓練權重的小型版本,“只有”1.17 億個參數。 * Jacob Devlin 和其它 Google 人員的[BERT 論文](https://links.jianshu.com/go?to=https%3A%2F%2Fhoml.info%2Fbert),也證明了在海量語料上做自監督預訓練的有效性,使用的是類似 GPT 的架構,但用的是無遮擋多頭注意力層(類似 Transformer 的編碼器)。這意味著模型實際是雙向的這就是 BERT(Bidirectional Encoder Representations from Transformers)中的 B 的含義。最重要的,作者提出了兩個預訓練任務,用以測試模型能力: 遮擋語言模型(MLM) 句子中的詞有 15 的概率被遮擋。訓練模型來預測被遮擋的詞。例如,如果原句是“She had fun at the birthday party”,模型的輸入是“She <mask> fun at the <mask> party”,讓模型來預測“had” 和 “birthday”(忽略其它輸出)。更加準確些,每個選出的單詞有 80%的概率被遮擋,10%的概率被替換為隨機詞(降低預訓練和微調的差異,因為模型在微調時看不到<mask> token),10%的概率不變(使模型偏向正確答案)。 預測下一句(NSP) 訓練模型預測兩句話是否是連續的。例如,模型可以預測“The dog sleeps”和“It snores loudly”是連續的,但是“The dog sleeps” 和 “The Earth orbits the Sun”是不連續的。這是一個有挑戰的任務,可以在微調任務,比如問答和銜接上,極大提高模型的性能。 可以看到,2018 年和 2019 年的創新是亞詞層面的 token 化,從 LSTM 轉向 Transformer,使用自監督學習預訓練語言模型,做細微的架構變動(或不變動)來微調模型。因為進展非常快,每人說得清明年流行的是什么。如今,流行的是 Transformer,但明天可能是 CNN(Maha Elbayad 在[2018 年的論文](https://links.jianshu.com/go?to=https%3A%2F%2Fhoml.info%2Fpervasiveattention),使用了遮擋的 2D 卷積層來做序列到序列任務)。如果卷土重來的話,也有可能是 RNN(例如,Shuai Li 在[2018 年的論文](https://links.jianshu.com/go?to=https%3A%2F%2Fhoml.info%2Findrnn)展示了,通過讓給定 RNN 層中的單元彼此獨立,可以訓練出更深的 RNN,能學習更長的序列)。 下一章,我們會學習用自編碼器,以無監督的方式學習深度表征,并用生成對抗網絡生成圖片及其它內容! ## 練習 1. 有狀態 RNN 和無狀態 RNN 相比,優點和缺點是什么? 2. 為什么使用編碼器-解碼器 RNN,而不是普通的序列到序列 RNN,來做自動翻譯? 3. 如何處理長度可變的輸入序列?長度可變的輸出序列怎么處理? 4. 什么是集束搜索,為什么要用集束搜索?可以用什么工具實現集束搜索? 5. 什么是注意力機制?用處是什么? 6. Transformer 架構中最重要的層是什么?它的目的是什么? 7. 什么時候需要使用采樣 softmax? 8. Hochreiter 和 Schmidhuber 在關于 LSTM 的[論文](https://links.jianshu.com/go?to=https%3A%2F%2Fhoml.info%2F93)中使用了嵌入 Reber 語法。這是一種人工的語法,用來生成字符串,比如 “BPBTSXXVPSEPE”。查看 Jenny Orr 對它的[介紹](https://links.jianshu.com/go?to=https%3A%2F%2Fhoml.info%2F108)。選擇一個嵌入 Reber 語法(比如 Jenny Orr 的論文中展示的),然后訓練一個 RNN 來判斷字符串是否符合語法。你需要先寫一個函數來生成訓練批次,其中 50%符合語法,50%不符合語法。 9. 訓練一個編碼器-解碼器模型,它可以將日期字符串從一個格式變為另一個格式(例如,從“April 22, 2019”變為“2019-04-22”)。 10. 閱讀 TensorFlow 的[《Neural Machine Translation with Attention tutorial》](https://links.jianshu.com/go?to=https%3A%2F%2Fhoml.info%2Fnmttuto)。 11. 使用一個最近的語言模型(比如,BERT),來生成一段更具信服力的莎士比亞文字。 參考答案見附錄 A。 {% endraw %}
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看