目前循環(huán)神經(jīng)網(wǎng)絡(luò)(RNN)已經(jīng)廣泛用于自然語言處理中,可以處理大量的序列數(shù)據(jù),可以說是最強大的神經(jīng)網(wǎng)絡(luò)模型之一。人們已經(jīng)給 RNN 找到了越來越多的事情做,比如畫畫和寫詩,微軟的小冰都已經(jīng)出版了一本詩集了。
而其實訓(xùn)練一個能寫詩的神經(jīng)網(wǎng)絡(luò)并不難,下面我們就介紹如何簡單快捷地建立一個會寫詩的網(wǎng)絡(luò)模型。
本次開發(fā)環(huán)境如下:
- Python 3.6
- Keras 環(huán)境
- Jupyter Notebook
整個過程分為以下步驟完成:
- 語料準備
- 語料預(yù)處理
- 模型參數(shù)配置
- 構(gòu)建模型
- 訓(xùn)練模型
- 模型作詩
- 繪制模型網(wǎng)絡(luò)結(jié)構(gòu)圖
下面一步步來構(gòu)建和訓(xùn)練一個會寫詩的模型。
第一,語料準備。一共四萬多首古詩,每行一首詩,標題在預(yù)處理的時候已經(jīng)去掉了。
第二,文件預(yù)處理。首先,機器并不懂每個中文漢字代表的是什么,所以要將文字轉(zhuǎn)換為機器能理解的形式,這里我們采用 One-Hot 的形式,這樣詩句中的每個字都能用向量來表示,下面定義函數(shù) preprocess_file() 來處理。
puncs = [']', '[', '(', ')', '{', '}', ':', '《', '》']
def preprocess_file(Config):
# 語料文本內(nèi)容
files_content = ''
with open(Config.poetry_file, 'r', encoding='utf-8') as f:
for line in f:
# 每行的末尾加上"]"符號代表一首詩結(jié)束
for char in puncs:
line = line.replace(char, "")
files_content += line.strip() + "]"
words = sorted(list(files_content))
words.remove(']')
counted_words = {}
for word in words:
if word in counted_words:
counted_words[word] += 1
else:
counted_words[word] = 1
# 去掉低頻的字
erase = []
for key in counted_words:
if counted_words[key] <= 2:
erase.append(key)
for key in erase:
del counted_words[key]
del counted_words[']']
wordPairs = sorted(counted_words.items(), key=lambda x: -x[1])
words, _ = zip(*wordPairs)
# word到id的映射
word2num = dict((c, i + 1) for i, c in enumerate(words))
num2word = dict((i, c) for i, c in enumerate(words))
word2numF = lambda x: word2num.get(x, 0)
return word2numF, num2word, words, files_content
在每行末尾加上 ] 符號是為了標識這首詩已經(jīng)結(jié)束了。我們給模型學(xué)習(xí)的方法是,給定前六個字,生成第七個字,所以在后面生成訓(xùn)練數(shù)據(jù)的時候,會以6的跨度,1的步長截取文字,生成語料。如果出現(xiàn)了 ] 符號,說明 ] 符號之前的語句和之后的語句是兩首詩里面的內(nèi)容,兩首詩之間是沒有關(guān)聯(lián)關(guān)系的,所以我們后面會舍棄掉包含 ] 符號的訓(xùn)練數(shù)據(jù)。
第三,模型參數(shù)配置。預(yù)先定義模型參數(shù)和加載語料以及模型保存名稱,通過類 Config 實現(xiàn)。
class Config(object):
poetry_file = 'poetry.txt'
weight_file = 'poetry_model.h5'
# 根據(jù)前六個字預(yù)測第七個字
max_len = 6
batch_size = 512
learning_rate = 0.001
第四,構(gòu)建模型,通過 PoetryModel 類實現(xiàn),類的代碼結(jié)構(gòu)如下:
class PoetryModel(object):
def __init__(self, config):
pass
def build_model(self):
pass
def sample(self, preds, temperature=1.0):
pass
def generate_sample_result(self, epoch, logs):
pass
def predict(self, text):
pass
def data_generator(self):
pass
def train(self):
pass
類中定義的方法具體實現(xiàn)功能如下:
(1)init 函數(shù)定義,通過加載 Config 配置信息,進行語料預(yù)處理和模型加載,如果模型文件存在則直接加載模型,否則開始訓(xùn)練。
def __init__(self, config):
self.model = None
self.do_train = True
self.loaded_model = False
self.config = config
# 文件預(yù)處理
self.word2numF, self.num2word, self.words, self.files_content = preprocess_file(self.config)
if os.path.exists(self.config.weight_file):
self.model = load_model(self.config.weight_file)
self.model.summary()
else:
self.train()
self.do_train = False
self.loaded_model = True
(2)build_model 函數(shù)主要用 Keras 來構(gòu)建網(wǎng)絡(luò)模型,這里使用 LSTM 的 GRU 來實現(xiàn),當然直接使用 LSTM 也沒問題。
def build_model(self):
'''建立模型'''
input_tensor = Input(shape=(self.config.max_len,))
embedd = Embedding(len(self.num2word)+1, 300, input_length=self.config.max_len)(input_tensor)
lstm = Bidirectional(GRU(128, return_sequences=True))(embedd)
dropout = Dropout(0.6)(lstm)
lstm = Bidirectional(GRU(128, return_sequences=True))(embedd)
dropout = Dropout(0.6)(lstm)
flatten = Flatten()(lstm)
dense = Dense(len(self.words), activation='softmax')(flatten)
self.model = Model(inputs=input_tensor, outputs=dense)
optimizer = Adam(lr=self.config.learning_rate)
self.model.compile(loss='categorical_crossentropy', optimizer=optimizer, metrics=['accuracy'])
(3)sample 函數(shù),在訓(xùn)練過程的每個 epoch 迭代中采樣。
def sample(self, preds, temperature=1.0):
'''
當temperature=1.0時,模型輸出正常
當temperature=0.5時,模型輸出比較open
當temperature=1.5時,模型輸出比較保守
在訓(xùn)練的過程中可以看到temperature不同,結(jié)果也不同
'''
preds = np.asarray(preds).astype('float64')
preds = np.log(preds) / temperature
exp_preds = np.exp(preds)
preds = exp_preds / np.sum(exp_preds)
probas = np.random.multinomial(1, preds, 1)
return np.argmax(probas)
(4)訓(xùn)練過程中,每個 epoch 打印出當前的學(xué)習(xí)情況。
def generate_sample_result(self, epoch, logs):
print("\n==================Epoch {}=====================".format(epoch))
for diversity in [0.5, 1.0, 1.5]:
print("------------Diversity {}--------------".format(diversity))
start_index = random.randint(0, len(self.files_content) - self.config.max_len - 1)
generated = ''
sentence = self.files_content[start_index: start_index + self.config.max_len]
generated += sentence
for i in range(20):
x_pred = np.zeros((1, self.config.max_len))
for t, char in enumerate(sentence[-6:]):
x_pred[0, t] = self.word2numF(char)
preds = self.model.predict(x_pred, verbose=0)[0]
next_index = self.sample(preds, diversity)
next_char = self.num2word[next_index]
generated += next_char
sentence = sentence + next_char
print(sentence)
(5)predict 函數(shù),用于根據(jù)給定的提示,來進行預(yù)測。
根據(jù)給出的文字,生成詩句,如果給的 text 不到四個字,則隨機補全。
def predict(self, text):
if not self.loaded_model:
return
with open(self.config.poetry_file, 'r', encoding='utf-8') as f:
file_list = f.readlines()
random_line = random.choice(file_list)
# 如果給的text不到四個字,則隨機補全
if not text or len(text) != 4:
for _ in range(4 - len(text)):
random_str_index = random.randrange(0, len(self.words))
text += self.num2word.get(random_str_index) if self.num2word.get(random_str_index) not in [',', '。',
','] else self.num2word.get(
random_str_index + 1)
seed = random_line[-(self.config.max_len):-1]
res = ''
seed = 'c' + seed
for c in text:
seed = seed[1:] + c
for j in range(5):
x_pred = np.zeros((1, self.config.max_len))
for t, char in enumerate(seed):
x_pred[0, t] = self.word2numF(char)
preds = self.model.predict(x_pred, verbose=0)[0]
next_index = self.sample(preds, 1.0)
next_char = self.num2word[next_index]
seed = seed[1:] + next_char
res += seed
return res
(6) data_generator 函數(shù),用于生成數(shù)據(jù),提供給模型訓(xùn)練時使用。
def data_generator(self):
i = 0
while 1:
x = self.files_content[i: i + self.config.max_len]
y = self.files_content[i + self.config.max_len]
puncs = [']', '[', '(', ')', '{', '}', ':', '《', '》', ':']
if len([i for i in puncs if i in x]) != 0:
i += 1
continue
if len([i for i in puncs if i in y]) != 0:
i += 1
continue
y_vec = np.zeros(
shape=(1, len(self.words)),
dtype=np.bool
)
y_vec[0, self.word2numF(y)] = 1.0
x_vec = np.zeros(
shape=(1, self.config.max_len),
dtype=np.int32
)
for t, char in enumerate(x):
x_vec[0, t] = self.word2numF(char)
yield x_vec, y_vec
i += 1
(7)train 函數(shù),用來進行模型訓(xùn)練,其中迭代次數(shù) number_of_epoch ,是根據(jù)訓(xùn)練語料長度除以 batch_size 計算的,如果在調(diào)試中,想用更小一點的 number_of_epoch ,可以自定義大小,把 train 函數(shù)的第一行代碼注釋即可。
def train(self):
#number_of_epoch = len(self.files_content) // self.config.batch_size
number_of_epoch = 10
if not self.model:
self.build_model()
self.model.summary()
self.model.fit_generator(
generator=self.data_generator(),
verbose=True,
steps_per_epoch=self.config.batch_size,
epochs=number_of_epoch,
callbacks=[
keras.callbacks.ModelCheckpoint(self.config.weight_file, save_weights_only=False),
LambdaCallback(on_epoch_end=self.generate_sample_result)
]
)
第五,整個模型構(gòu)建好以后,接下來進行模型訓(xùn)練。
model = PoetryModel(Config)
訓(xùn)練過程中的第1-2輪迭代:
訓(xùn)練過程中的第9-10輪迭代:
雖然訓(xùn)練過程寫出的詩句不怎么能看得懂,但是可以看到模型從一開始標點符號都不會用 ,到最后寫出了有一點點模樣的詩句,能看到模型變得越來越聰明了。
第六,模型作詩,模型迭代10次之后的測試,首先輸入幾個字,模型根據(jù)輸入的提示,做出詩句。
text = input("text:")
sentence = model.predict(text)
print(sentence)
比如輸入:小雨,模型做出的詩句為:
輸入:text:小雨
結(jié)果:小妃侯里守。雨封即客寥。俘剪舟過槽。傲老檳冬絳。
第七,繪制網(wǎng)絡(luò)結(jié)構(gòu)圖。
模型結(jié)構(gòu)繪圖,采用 Keras自帶的功能實現(xiàn):
plot_model(model.model, to_file='model.png')
得到的模型結(jié)構(gòu)圖如下:
本節(jié)使用 LSTM 的變形 GRU 訓(xùn)練出一個能作詩的模型,當然大家可以替換訓(xùn)練語料為歌詞或者小說,讓機器人自動創(chuàng)作不同風(fēng)格的歌曲或者小說。
參考文獻以及推薦閱讀: