- 首先,因爲我們想訓練的是寫詩的內容,因此等下訓練的時候只需要詩的內容即可。
- 另外,我們的數據中可能存在部分符號的問題,例如中英文符號混用、每行存在多個冒號、數據中存在其他符號等問題,因此我們需要對數據進行清洗。
DATA_PATH = './datasets/poetry.txt'
MAX_LEN = 64
DISALLOWED_WORDS = ['(', ')', '(', ')', '__', '《', '》', '【', '】', '[', ']']
poetry = []
with open(DATA_PATH, 'r', encoding='utf-8') as f:
lines = f.readlines()
for line in lines:
fields = re.split(r"[::]", line)
if len(fields) != 2:
continue
content = fields[1]
if len(content) > MAX_LEN - 2:
continue
if any(word in content for word in DISALLOWED_WORDS):
continue
poetry.append(content.replace('\n', ''))
- 接着,我們來打印幾首處理後的詩看看
for i in range(0, 5):
print(poetry[i])
繫馬宮槐老,持杯店菊黃。故交今不見,流恨滿川光。
世間何事不潸然,得失人情命不延。適向蔡家廳上飲,回頭已見一千年。
只領千餘騎,長驅磧邑間。雲州多警急,雪夜度關山。石響鈴聲遠,天寒弓力慳。秦樓休悵望,不日凱歌還。
今日花前飲,甘心醉數杯。但愁花有語,不爲老人開。
秋來吟更苦,半咽半隨風。禪客心應亂,愁人耳願聾。雨晴煙樹裏,日晚古城中。遠思應難盡,誰當與我同。
- 現在,我們需要對詩句進行分詞,不過考慮到爲了最後生成的詩的長度的整齊性,以及便利性,我們在這裏按單個字符進行拆分。(你也可以使用專業的分詞工具,例如jieba、hanlp等)
- 並且,我們還需要統計一下詞頻,刪除掉出現次數較低的詞
MIN_WORD_FREQUENCY = 8
counter = Counter()
for line in poetry:
counter.update(line)
tokens = [token for token, count in counter.items() if count >= MIN_WORD_FREQUENCY]
- 看看我們的詞頻統計結果如何
i = 0
for token, count in counter.items():
if i >= 5:
break;
print(token, "->",count)
i += 1
寒 -> 2627
隨 -> 1039
窮 -> 487
律 -> 119
變 -> 286
- 除此之外,還有幾個點需要我們考慮
- 需要用2個符號分別表示一首詩的起始點、結束點。這樣我們的神經網絡才能由訓練得知什麼時候寫完一首詩。
- 需要一個字符來代表所有未知的字符。因爲我們的數據去除了低頻詞,並且我們的文本不可能包含全世界所有的字符,因此需要一個字符來表示未知字符。
- 需要一個字符來填充詩詞,以保證詩詞的長度統一。因爲單個批次內訓練的數據特徵長度必須一致。
- 因此,我們需要設置幾個特殊字符
tokens = ["[PAD]", "[NONE]", "[START]", "[END]"] + tokens
- 最後,我們需要對生成的所有詞進行編號,方便後面進行轉碼
word_idx = {}
idx_word = {}
for idx, word in enumerate(tokens):
word_idx[word] = idx
idx_word[idx] = word
- 注意:因爲後面我們要構建一個Tokenizer,在其內部實現該結構,此處的代碼可以不用管
- 爲了方便後面按批次抽取數據訓練模型,因此我們還需要構建一個數據生成器。這樣TensorFlow在訓練模型時會之間從該數據生成器抽取數據。
- 另外,我們抽取的原始數據還需要進行轉碼,才能餵給模型進行訓練,該部分也封裝在PoetryDataSet中
- 其代碼如下
class PoetryDataSet:
"""
古詩數據集生成器
"""
def __init__(self, data, tokenizer, batch_size):
self.data = data
self.total_size = len(self.data)
self.tokenizer = tokenizer
self.batch_size = BATCH_SIZE
self.steps = int(math.floor(len(self.data) / self.batch_size))
def pad_line(self, line, length, padding=None):
"""
對齊單行數據
"""
if padding is None:
padding = self.tokenizer.pad_id
padding_length = length - len(line)
if padding_length > 0:
return line + [padding] * padding_length
else:
return line[:length]
def __len__(self):
return self.steps
def __iter__(self):
np.random.shuffle(self.data)
for start in range(0, self.total_size, self.batch_size):
end = min(start + self.batch_size, self.total_size)
data = self.data[start:end]
max_length = max(map(len, data))
batch_data = []
for str_line in data:
encode_line = self.tokenizer.encode(str_line)
pad_encode_line = self.pad_line(encode_line, max_length + 2)
batch_data.append(pad_encode_line)
batch_data = np.array(batch_data)
yield batch_data[:, :-1], batch_data[:, 1:]
def generator(self):
while True:
yield from self.__iter__()
- 生成的特徵、標籤的示例如下(實際是編號,此處做了轉換)
特徵:[START]我有辭鄉劍,玉鋒堪截雲。襄陽走馬客,意氣自生春。朝嫌劍花淨,暮嫌劍光冷。能持劍向人,不解持照身。[END][PAD][PAD][PAD]
標籤:我有辭鄉劍,玉鋒堪截雲。襄陽走馬客,意氣自生春。朝嫌劍花淨,暮嫌劍光冷。能持劍向人,不解持照身。[END][PAD][PAD][PAD][PAD]
- 初始化 PoetryDataSet
BATCH_SIZE = 32
dataset = PoetryDataSet(poetry, tokenizer, BATCH_SIZE)
- 現在我們可以開始構建RNN模型了,因爲模型層與層之間是順序的,因此我們可以採用Sequential快速構建模型。
- 模型如下 (不太懂LSTM?建議看看這堂課程)
model = tf.keras.Sequential([
tf.keras.layers.Embedding(input_dim=tokenizer.dict_size, output_dim=150),
tf.keras.layers.LSTM(150, dropout=0.5, return_sequences=True),
tf.keras.layers.LSTM(150, dropout=0.5, return_sequences=True),
tf.keras.layers.TimeDistributed(tf.keras.layers.Dense(tokenizer.dict_size, activation='softmax')),
])
- 模型總覽
model.summary()
Model: "sequential_2"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
embedding_2 (Embedding) (None, None, 150) 515100
_________________________________________________________________
lstm_4 (LSTM) (None, None, 150) 180600
_________________________________________________________________
lstm_5 (LSTM) (None, None, 150) 180600
_________________________________________________________________
time_distributed_2 (TimeDist (None, None, 3434) 518534
=================================================================
Total params: 1,394,834
Trainable params: 1,394,834
Non-trainable params: 0
_________________________________________________________________
- 進行模型編譯(選擇優化器、損失函數)
model.compile(
optimizer=tf.keras.optimizers.Adam(),
loss=tf.keras.losses.sparse_categorical_crossentropy
)
- 注意:因爲我們的標籤是非one_hot形式的,因此需要選擇sparse_categorical_crossentropy 。當然你也可以利用tf.one_hot(標籤, size)進行轉換,然後使用categorical_crossentropy。
- 模型對於數據的預測結果是概率分佈
token_ids = [tokenizer.token_to_id(word) for word in ["月", "光", "靜", "謐"]]
result = model.predict([token_ids ,])
print(result)
[[[2.0809230e-04 9.3881181e-03 5.5695949e-07 ... 5.6030808e-06
8.5241054e-06 2.0507096e-06]
[7.6916285e-06 6.1246334e-03 1.8850582e-08 ... 4.8418292e-06
2.8483141e-06 5.3288642e-07]
[5.0856406e-06 3.1365673e-03 1.9067786e-08 ... 4.5156207e-06
1.0479171e-05 9.7814757e-07]
[7.1793047e-06 2.2729969e-02 2.0391434e-08 ... 2.0609916e-06
2.2420336e-06 2.1413473e-06]]]
- 每次預測其實是根據一個序列預測一個新的詞,我們需要詞的多樣化,因此可以按預測結果的概率分佈進行抽樣。代碼如下
def predict(model, token_ids):
"""
在概率值爲前100的詞中選取一個詞(按概率分佈的方式)
:return: 一個詞的編號(不包含[PAD][NONE][START])
"""
_probas = model.predict([token_ids, ])[0, -1, 3:]
p_args = _probas.argsort()[-100:][::-1]
p = _probas[p_args]
p = p / sum(p)
target_index = np.random.choice(len(p), p=p)
return p_args[target_index] + 3
- 我們隨便來對一個序列進行循環預測試試
token_ids = tokenizer.encode("清風明月")[:-1]
while len(token_ids) < 13:
target = predict(model, token_ids)
token_ids.append(target)
if target == tokenizer.end_id:
break
print("".join(tokenizer.decode(token_ids)))
清風明月夜,晚色北堂殘。
- 至此,基本的預測已經完成。後面只需要設置一些規則,就可以實現隨機生成一首詩、生成一首藏頭詩的功能
- 代碼如下
def generate_random_poem(tokenizer, model, text=""):
"""
隨機生成一首詩
:param tokenizer: 分詞器
:param model: 古詩模型
:param text: 古詩的起始字符串,默認爲空
:return: 一首古詩的字符串
"""
token_ids = tokenizer.encode(text)[:-1]
while len(token_ids) < MAX_LEN:
target = predict(model, token_ids)
token_ids.append(target)
if target == tokenizer.end_id:
break
return "".join(tokenizer.decode(token_ids))
- 隨意測試幾次
for i in range(5):
print(generate_random_poem(tokenizer, model))
江亭路斷暮,歸去見芳洲。惆悵門中去,心年少地深。夜期深木靜,水落夕陽深。秋去人南雨,悽頭望海中。
洛陌江陽宮下樹,玉門宮夜似東雲。今更已長逢醉士,一明先語似相春。
春山風半夜初歸,萬歲空聲去去過。自惜秦生猶送酒,何人無計不安稀。
何處東陵路,無年已復還。曉鶯逢半急,潮望月雲稀。暗影通三度,煙沙水鳥深。當年相憶望,何處問漁家。
清夜向陽閣,一風看北宮。雨分紅蕊草,紅杏藥茶行。野石翻山遠,猿晴不獨天。誰知一山下,飛首卻悠悠。
- 給一首詩的開頭,讓它自己續寫
print(generate_random_poem(tokenizer, model, "春眠不覺曉,"))
print(generate_random_poem(tokenizer, model, "白日依山盡,"))
print(generate_random_poem(tokenizer, model, "秦時明月漢時關,"))
春眠不覺曉,坐住樹深空。風月飄猶曉,春多出水流。
白日依山盡,相逢獨水聲。唯疑見心意,一老淚鳴歸。落晚南遊客,吟猿見柳寒。何堪看暮望,還見有軍情。
秦時明月漢時關,慾望時恩不道心。莫憶舊鄉僧雁在,始堪曾在牡苓流。