閱讀273 返回首頁    go 阿裏雲 go 技術社區[雲棲]


用Keras開發字符級神經網絡語言模型

語言模型可根據序列中出現的特定單詞來預測下一個單詞。可以使用神經網絡在字符級別上開發語言模型。基於字符的語言模型有一個最大的優點,就是在處理單詞、標點符號和其他文檔結構的時候,能保持較小的詞匯量和較強的靈活性。但所付出的代價是模型較大、訓練較慢。然而,在神經網絡語言模型領域,基於字符的模型為語言建模提供了一種通用、靈活和強大的方法。

在本教程中,你將了解到如何開發基於字符的神經網絡語言模型。

學習完本教程,你將學會:

  • 如何針對基於字符的語言建模準備文本。
  • 如何使用LSTM開發基於字符的語言模型。
  • 如何使用訓練過的基於字符的語言模型來生成文本。

教程概述

本教程分為四個部分:

  1. Sing a Song of Sixpence(譯者注:一首英文童謠)
  2. 數據準備
  3. 訓練語言模型
  4. 生成文本

Sing a Song of Sixpence

童謠“Sing a Song of Sixpence”在西方人人都會唱。我們將用它來開發基於字符的語言模型。

這首童謠很短,所以模型的擬合會很快,但不能太短,那樣我們就不會看到任何有意思的東西。下麵是這首童謠完整歌詞:

Sing a song of sixpence,
A pocket full of rye.
Four and twenty blackbirds,
Baked in a pie.

When the pie was opened
The birds began to sing;
Wasn’t that a dainty dish,
To set before the king.

The king was in his counting house,
Counting out his money;
The queen was in the parlour,
Eating bread and honey.

The maid was in the garden,
Hanging out the clothes,
When down came a blackbird
And pecked off her nose.

複製這段文本,並將其保存到當前工作目錄中的一個新文件中,文件名為“rhyme.txt”。

數據準備

第一步是準備文本數據。我們將首先定義語言模型的類型。

語言模型設計

語言模型必須用文本進行訓練,在基於字符的語言模型中,輸入和輸出序列必須是字符。用於輸入的字符的個數決定了需要提供給模型以引出第一個預測字符的字符數。在第一個字符生成之後,可以將其添加到輸入序列上,作為模型的輸入以生成下一個字符。

序列越長,則為模型提供的上下文也越多,同時,模型將耗費更長的時間來進行訓練。我們這個模型使用的字符的長度是10。

下麵我們將把原始文本轉換成模型可以學習的形式。

加載文本

童謠的歌詞必須加載到內存之後才能使用。下麵是一個名為load_doc()的函數,用於加載指定文件名的文本文件並返回加載的文本。

# load doc into memory
def load_doc(filename):
    # open the file as read only
    file = open(filename, 'r')
    # read all text
    text = file.read()
    # close the file
    file.close()
    return text

可以使用這個函數來加載名為“rhyme.txt”的文件,並將內容放入內存中。然後將文件的內容打印到屏幕上進行完整性檢查。

# load text
raw_text = load_doc('rhyme.txt')
print(raw_text)

淨化文本

接下來,需要淨化加載的文本。

這裏我們不會做太多的事情,隻是刪除所有的換行符,轉換成一段按空格進行分割的長字符序列。

# clean
tokens = raw_text.split()
raw_text = ' '.join(tokens)

你可能需要探索一下淨化數據的其他一些方法,例如將文本轉換為小寫字母或刪除標點符號,以減少最終的詞匯量,這樣可以開發出更小、更精簡的模型。

創建序列

長字符列表有了,下麵就可以創建用於訓練模型的輸入輸出序列了。

每個輸入序列包含十個字符和一個輸出字符,因此,每個序列包含了11個字符。我們可以通過枚舉文本中的字符來創建序列,從索引為10也就是第11個字符開始。

# organize into sequences of characters
length = 10
sequences = list()
for i in range(length, len(raw_text)):
    # select sequence of tokens
    seq = raw_text[i-length:i+1]
    # store
    sequences.append(seq)
print('Total Sequences: %d' % len(sequences))

運行這段代碼,我們可以看到,用來訓練語言模型的序列其實隻有不到400個字符。

Total Sequences: 399

保存序列

最後,將準備好的數據保存到文件中,後麵在開發模型的時候再加載。

下麵是save_doc()函數,給定字符串列表和文件名,將字符串保存到文件中,每行一個字符串。

# save tokens to file, one dialog per line
def save_doc(lines, filename):
    data = '\n'.join(lines)
    file = open(filename, 'w')
    file.write(data)
    file.close()

調用這個函數,將準備好的序列保存到當前工作目錄下的“char_sequences.txt”文件中。

# save sequences to file
out_filename = 'char_sequences.txt'
save_doc(sequences, out_filename)

完整的例子

將上麵那些代碼片段組合到一起,組成下麵這份完整的代碼:

# load doc into memory
def load_doc(filename):
    # open the file as read only
    file = open(filename, 'r')
    # read all text
    text = file.read()
    # close the file
    file.close()
    return text

# save tokens to file, one dialog per line
def save_doc(lines, filename):
    data = '\n'.join(lines)
    file = open(filename, 'w')
    file.write(data)
    file.close()

# load text
raw_text = load_doc('rhyme.txt')
print(raw_text)

# clean
tokens = raw_text.split()
raw_text = ' '.join(tokens)

# organize into sequences of characters
length = 10
sequences = list()
for i in range(length, len(raw_text)):
    # select sequence of tokens
    seq = raw_text[i-length:i+1]
    # store
    sequences.append(seq)
print('Total Sequences: %d' % len(sequences))

# save sequences to file
out_filename = 'char_sequences.txt'
save_doc(sequences, out_filename)

運行該示例,將生成“char_seqiences.txt”文件,內容如下:

Sing a song
ing a song
ng a song o
g a song of
 a song of
a song of s
 song of si
song of six
ong of sixp
ng of sixpe
...

下麵準備訓練基於字符的神經語言模型。

訓練語言模型

本章節將為上麵準備好的序列數據開發一個神經語言模型。該模型將讀取已經編碼的字符,並預測出序列的下一個字符。

加載數據

第一步是從"char_sequences.txt"加載字符序列數據。

我們可以使用上一章節中開發的load_doc()函數。載入後,將文本按換行符進行分割以得到序列列表。

# load doc into memory
def load_doc(filename):
    # open the file as read only
    file = open(filename, 'r')
    # read all text
    text = file.read()
    # close the file
    file.close()
    return text

# load
in_filename = 'char_sequences.txt'
raw_text = load_doc(in_filename)
lines = raw_text.split('\n')

序列編碼

字符序列必須編碼為整數。也就是說每個字符都會被分配一個指定的整數值,每個字符序列都會被編碼為一個整數序列。

我們可以根據原始輸入數據來創建映射關係。該映射關係是字符值映射到整數值的字典。

chars = sorted(list(set(raw_text)))
mapping = dict((c, i) for i, c in enumerate(chars))

接下來,逐個處理每個字符序列,並使用字典映射來查找每個字符的整數值。

sequences = list()
for line in lines:
    # integer encode line
    encoded_seq = [mapping[char] for char in line]
    # store
    sequences.append(encoded_seq)

運行的結果是整數序列列表。

字典映射表的大小即詞匯表的大小。

# vocabulary size
vocab_size = len(mapping)
print('Vocabulary Size: %d' % vocab_size)

運行這段代碼,我們可以看到輸入數據中的字符剔重後有38個。

Vocabulary Size: 38

分割輸入和輸出

現在,序列已經被編碼成整數了,下麵可以將列分割成輸入和輸出字符序列。可以使用一個簡單的數組切片來完成此操作。

sequences = array(sequences)
X, y = sequences[:,:-1], sequences[:,-1]

接下來,將對每個字符進行獨熱編碼,也就是說,每個字符都會變成一個向量。這為神經網絡提供了更精確的輸入表示,還為網絡預測提供了一個明確的目標。

我們可以使用Keras API中的to_categorical()函數來對輸入和輸出序列進行獨熱編碼。

sequences = [to_categorical(x, num_classes=vocab_size) for x in X]
X = array(sequences)
y = to_categorical(y, num_classes=vocab_size)

現在,我們已經為模型的擬合做好準備了。

模型擬合

該模型使用了一個針對獨熱編碼輸入序列采用10個時間步長和38個特征的輸入層進行定義。我們在X輸入數據上使用第二和第三個維度,而不是指定這些數字。這是因為當序列的長度或詞匯表的大小發生改變的實話,無需改變模型的定義。

該模型有一個包含75個存儲器單元的LSTM隱藏層,通過一些試驗和錯誤進行選擇。

該模型有一個完全連接的輸出層,輸出一個詞匯表中所有字符概率分布的向量。在輸出層上使用softmax激活函數來確保輸出具有概率分布的屬性。

# define model
model = Sequential()
model.add(LSTM(75, input_shape=(X.shape[1], X.shape[2])))
model.add(Dense(vocab_size, activation='softmax'))
print(model.summary())

運行這段代碼將打印出網絡的概要信息以進行完整性檢查。

_________________________________________________________________
Layer (type)                 Output Shape              Param #
=================================================================
lstm_1 (LSTM)                (None, 75)                34200
_
dense_1 (Dense)              (None, 38)                2888
=================================================================
Total params: 37,088
Trainable params: 37,088
Non-trainable params: 0
_

該模型將執行100次訓練迭代來進行擬合。

# compile model
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
# fit model
model.fit(X, y, epochs=100, verbose=2)

保存模型

模型擬合完成之後,將其保存到文件以備後麵使用。Keras提供了save()函數,可以使用該函數將模型保存到單個文件中,包括權重和拓撲信息。

# save the model to file
model.save('model.h5')

另外還要保存從字符到整數的映射關係,因為在使用模型的時候,需要對任意的輸入進行編碼,並對模型的輸出進行解碼。

# save the mapping
dump(mapping, open('mapping.pkl', 'wb'))

完整的例子

將上麵那些代碼片段組合到一起,組成下麵這份基於字符的神經網絡語言模型的完整代碼:

from numpy import array
from pickle import dump
from keras.utils import to_categorical
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import LSTM

# load doc into memory
def load_doc(filename):
    # open the file as read only
    file = open(filename, 'r')
    # read all text
    text = file.read()
    # close the file
    file.close()
    return text

# load
in_filename = 'char_sequences.txt'
raw_text = load_doc(in_filename)
lines = raw_text.split('\n')

# integer encode sequences of characters
chars = sorted(list(set(raw_text)))
mapping = dict((c, i) for i, c in enumerate(chars))
sequences = list()
for line in lines:
    # integer encode line
    encoded_seq = [mapping[char] for char in line]
    # store
    sequences.append(encoded_seq)

# vocabulary size
vocab_size = len(mapping)
print('Vocabulary Size: %d' % vocab_size)

# separate into input and output
sequences = array(sequences)
X, y = sequences[:,:-1], sequences[:,-1]
sequences = [to_categorical(x, num_classes=vocab_size) for x in X]
X = array(sequences)
y = to_categorical(y, num_classes=vocab_size)

# define model
model = Sequential()
model.add(LSTM(75, input_shape=(X.shape[1], X.shape[2])))
model.add(Dense(vocab_size, activation='softmax'))
print(model.summary())
# compile model
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
# fit model
model.fit(X, y, epochs=100, verbose=2)

# save the model to file
model.save('model.h5')
# save the mapping
dump(mapping, open('mapping.pkl', 'wb'))

這個例程的運行時間可能需要一分鍾。

...
Epoch 96/100
0s - loss: 0.2193 - acc: 0.9950
Epoch 97/100
0s - loss: 0.2124 - acc: 0.9950
Epoch 98/100
0s - loss: 0.2054 - acc: 0.9950
Epoch 99/100
0s - loss: 0.1982 - acc: 0.9950
Epoch 100/100
0s - loss: 0.1910 - acc: 0.9950

運行結束之後,會在當前目錄生成兩個文件,model.h5mapping.pkl

接下來,看一下如何使用這個學習過的模型。

生成文本

我們將使用這個學習過的語言模型來生成具有相同統計特性的新的文本序列。

加載模型

第一步是加載文件“model.h5”中的模型,可以使用Keras API中的load_model()函數進行加載。

# load the model
model = load_model('model.h5')

還需要加載文件“mapping.pkl”文件中的字典,用於將字符映射為整數。

# load the mapping
mapping = load(open('mapping.pkl', 'rb'))

下麵可以使用這個模型了。

生成字符

為了啟動生成過程,必須提供包含10個字符的序列作為模型的輸入。

首先,字符序列必須使用加載進來的映射關係編碼為整數值。

# encode the characters as integers
encoded = [mapping[char] for char in in_text]

接下來,使用Keras中的pad_sequences()函數對整數值進行獨熱編碼,並將序列重塑為三個維度。因為我們隻有一個序列,而且LSTM需要所有的輸入都有三個維度(樣本、時間步長、特征)。

# one hot encode
encoded = to_categorical(encoded, num_classes=len(mapping))
encoded = encoded.reshape(1, encoded.shape[0], encoded.shape[1])

下麵,就可以使用模型來預測序列中的下一個字符了。

使用predict_classes()而不是predict()來直接選擇具有最高概率的字符整數。

# predict character
yhat = model.predict_classes(encoded, verbose=0)

可以通過查找映射中的整數-字符關係來對整數進行解碼。

out_char = ''
for char, index in mapping.items():
    if index == yhat:
        out_char = char
        break

這個字符隨後可以添加到輸入序列中去。然後通過截斷輸入序列文本中的第一個字符來確保輸入序列是10個字符的長度。可以使用Keras API中的pad_sequences()函數來執行截斷操作。

把上麵這些放在一起,定義一個名為generate_seq()的新函數來使用模型生成新的文本序列。

# generate a sequence of characters with a language model
def generate_seq(model, mapping, seq_length, seed_text, n_chars):
    in_text = seed_text
    # generate a fixed number of characters
    for _ in range(n_chars):
        # encode the characters as integers
        encoded = [mapping[char] for char in in_text]
        # truncate sequences to a fixed length
        encoded = pad_sequences([encoded], maxlen=seq_length, truncating='pre')
        # one hot encode
        encoded = to_categorical(encoded, num_classes=len(mapping))
        encoded = encoded.reshape(1, encoded.shape[0], encoded.shape[1])
        # predict character
        yhat = model.predict_classes(encoded, verbose=0)
        # reverse map integer to character
        out_char = ''
        for char, index in mapping.items():
            if index == yhat:
                out_char = char
                break
        # append to input
        in_text += char
    return in_text

完整的例子

將上麵那些代碼片段組合到一起,組成下麵這份基於字符的神經網絡語言模型的完整代碼:

from pickle import load
from keras.models import load_model
from keras.utils import to_categorical
from keras.preprocessing.sequence import pad_sequences

# generate a sequence of characters with a language model
def generate_seq(model, mapping, seq_length, seed_text, n_chars):
    in_text = seed_text
    # generate a fixed number of characters
    for _ in range(n_chars):
        # encode the characters as integers
        encoded = [mapping[char] for char in in_text]
        # truncate sequences to a fixed length
        encoded = pad_sequences([encoded], maxlen=seq_length, truncating='pre')
        # one hot encode
        encoded = to_categorical(encoded, num_classes=len(mapping))
        encoded = encoded.reshape(1, encoded.shape[0], encoded.shape[1])
        # predict character
        yhat = model.predict_classes(encoded, verbose=0)
        # reverse map integer to character
        out_char = ''
        for char, index in mapping.items():
            if index == yhat:
                out_char = char
                break
        # append to input
        in_text += char
    return in_text

# load the model
model = load_model('model.h5')
# load the mapping
mapping = load(open('mapping.pkl', 'rb'))

# test start of rhyme
print(generate_seq(model, mapping, 10, 'Sing a son', 20))
# test mid-line
print(generate_seq(model, mapping, 10, 'king was i', 20))
# test not in original
print(generate_seq(model, mapping, 10, 'hello worl', 20))

運行該示例將生成三個文本序列。

第一個是測試這個模型在從童謠的開頭進行預測的話表現如何。第二個是測試從某一行的中間開始預測表現如何。最後是測試模型遇到從未見過的字符序列時表現如何。

Sing a song of sixpence, A poc
king was in his counting house
hello worls e pake wofey. The

我們可以看到,這個模型在前兩個例子中的表現得還不錯,符合預期。但對於新的文本來說,預測的結果就有點匪夷所思了。

總結

通過閱讀本教程,你已經學會了如何開發基於字符的神經網絡語言模型,包括:

  • 如何針對基於字符的語言建模準備文本。
  • 如何使用LSTM開發基於字符的語言模型。
  • 如何使用訓練過的基於字符的語言模型來生成文本。

文章原標題《How to Develop a Character-Based Neural Language Model in Keras》,作者: Jason Brownlee,譯者:夏天,審校:主題曲。

文章為簡譯,更為詳細的內容請查看原文

本文由北郵@愛可可-愛生活老師推薦,阿裏雲雲棲社區組織翻譯。

最後更新:2017-11-13 11:34:24

  上一篇:go  共享鏈係統平台開發
  下一篇:go  Master-Slave Synchronization for MySQL