這裏稍微更改了下《深度學習框架PyTorch:入門與實踐》裏的demo,去掉稍微繁瑣和多次訓練的部分,只保留了比較核心的生成連接詩和藏頭詩兩部分(比較渣太複雜了看不懂)。
目標效果:
連接詩:
機器學習書,局上無酒漿。婆娑珍金盤,縷爛金葳漿。萱草發秋葉,旖旎鏤金牆。拳芳既盈薄,祿位不敢匼。揆我不及飽,有時不相併。我爲蘧生意,日出獄所宣。徇祿懲未卜,退食何由嘗。濡毫若可濯,視事忘憂傷。茍餐償朽援,亦有糟醨漿。憶來不得意,與我來此方。幽谷蔭幽壤,清猨鳴秋霜。因思霖雨霽,始覺風物長。既與物外違,復知辱所並。吾欲顧己仁,豈必死者傷。茍哉吾安適,複道邁一觴。雖遇物外興,永忻良所侵。
藏頭詩:
機心儼不見,金馬不可量。器食非所古,此心何足傷。學書三十載,相勸一旬長。習隸不可樂,持斧亦葳裳。書來不可再,稅首皆自忙。
主要面main.py:
import torch as t
from data import get_data
from model import PoetryModel
"""對象"""
class Config(object):
data_path = 'data/' # 詩歌的文本文件存放路徑
pickle_path = 'tang.npz' # 預處理好的二進制文件,包含data,形狀爲(57580,125),共57580首詩歌,每首詩歌長度125,不夠補空格,多餘丟棄
category = 'poet.tang' # 類別,唐詩還是宋詩歌(poet.song)
max_gen_len = 200 # 生成詩歌最長長度
prefix_words = '細雨魚兒出,微風燕子斜。' # 不是詩歌的組成部分,用來控制生成詩歌的意境
start_words = '閒雲潭影日悠悠' # 詩歌開始
acrostic = True # 是否是藏頭詩
model_path = None # 預訓練模型路徑
opt = Config()
"""給定一句詩,繼續生成一首完整的詩"""
def generate(model, start_words, ix2word, word2ix, prefix_words=None):
"""
ix2word:每個序號對應的字
word2ix:每個字對應的序號
prefix_words:詩歌意境
<EOP>:8290
<START>:8591
</s>:8292
<,>:7066
<。>:7435
"""
results = list(start_words)
start_word_len = len(start_words)
input = t.Tensor([word2ix['<START>']]).view(1, 1).long() # 手動設置第一個詞爲<START>
hidden = None
if prefix_words: #意境詩句存在,這裏主要用於訓練記憶hidden,生成的output無用
for word in prefix_words:
output, hidden = model(input, hidden)
input = input.data.new([word2ix[word]]).view(1, 1)
for i in range(opt.max_gen_len): #最大的句子長度
output, hidden = model(input, hidden) #前邊幾個output無用,因爲默認使用前綴詩句
if i < start_word_len: #如果小於前綴句子的長度
w = results[i] #取前綴對應位置的字
input = input.data.new([word2ix[w]]).view(1, 1) #取出前綴詩句對應的字的下標,形成1*1的矩陣
else: #已經輸出完前綴了,該保存output中計算的字了
top_index = output.data[0].topk(1)[1][0].item() #保存output中概率最大的那個字的下標
w = ix2word[top_index] #取出這個下標對應的字
results.append(w) #生成的詩句序列加入剛生成的結果
input = input.data.new([top_index]).view(1, 1) #把當前字的下標擴充成1*1當作下一次的輸入
if w == '<EOP>': #如果預測到了結束,刪掉並退出
del results[-1]
break
return results
"""給定一句詩,對應生成藏頭詩"""
def gen_acrostic(model, start_words, ix2word, word2ix, prefix_words=None):
"""
ix2word:每個序號對應的字
word2ix:每個字對應的序號
prefix_words:詩歌意境
<EOP>:8290
<START>:8591
</s>:8292
<,>:7066
<。>:7435
"""
results = [] #用於存儲生成的詩歌
start_word_len = len(start_words) #用於生成藏頭詩的詩句的長度(有幾個字生成幾句)
input = (t.Tensor([word2ix['<START>']]).view(1, 1).long()) #word2ix['<START>']=8291,一開始input先賦值<START>的序號
hidden = None
index = 0 # 用來指示已經生成了多少句藏頭詩
pre_word = '<START>' #用來表示上一個詞,第一個詞設置爲'<START>'
if prefix_words: #如果設置了意境詩句,其中逗號和句號也包括在內
for word in prefix_words: #遍歷這句詩的每一個字
output, hidden = model(input, hidden) #input是1*1矩陣,只包含上一個字,用於輸進去進行降維,和一個空的hidden記憶,輸出關於當前字的記憶hidden和當前的輸出output,這個output貌似沒用
input = (input.data.new([word2ix[word]])).view(1, 1) #將當前遍歷的字的編號取出形成一個1*1矩陣賦值給input,input.data.new([word2ix[word]])是將一個數字變成一維tensor,view(1,1)是變成2維tensor
for i in range(opt.max_gen_len): #生成詩歌最長長度
output, hidden = model(input, hidden) #對之前意境的最後一個字進行處理,這裏其實是句號,output代表對下一個字的預測概率
top_index = output.data[0].topk(1)[1][0].item() #topk(1)取出向量中一個最大值,返回概率和對應下標
w = ix2word[top_index] #獲取機率最大的下標對應的字
if (pre_word in {'。', '!', '<START>'}): # 如果遇到句號歎號或者開始符號,藏頭的詞送進去生成
if index == start_word_len: #index用來指示已經生成了多少句藏頭詩,如果相等則代表已經生成完了,退出循環
break
else: # 把藏頭的詞作爲輸入送入模型
w = start_words[index] #取藏頭詩中第index個下標對應的字(這裏如果是每一句開頭,會拋棄上一句最後取出來的最大概率的w,重新覆蓋了)
index += 1
input = (input.data.new([word2ix[w]])).view(1, 1) #取對應字的數字編號作爲一個1*1矩陣,作爲下一個詞的輸入
else: # 否則的話,把上一次預測是詞作爲下一個詞輸入
input = (input.data.new([word2ix[w]])).view(1, 1)
results.append(w) #結果詩句添加當前字
pre_word = w #將w賦值給變量作爲前一個單詞
return results
"""提供接口,處理命令選擇生成詩句類型"""
def gen(kwargs):
for k, v in kwargs.items(): #遍歷出來的k是鍵,v是值
setattr(opt, k, v) #設置屬性值,第一個參數是對象,第二個參數是屬性,第三個參數是屬性值
data, word2ix, ix2word = get_data(opt) #第一個參數每一行是一首詩對應的字的下標,第二個參數是每個字對應的序號,第三個參數是每個序號對應的字
model = PoetryModel(len(word2ix), 128, 256)
map_location = lambda s, l: s #這三句貌似是將模型加載在GPU上
state_dict = t.load(opt.model_path, map_location=map_location)
model.load_state_dict(state_dict)
gen_poetry = gen_acrostic if opt.acrostic else generate #是不是藏頭詩,gen_acrostic爲將提示句拆成每句第一個字生成藏頭詩,generate爲將提示句作爲第一句
result = gen_poetry(model, opt.start_words, ix2word, word2ix, opt.prefix_words) #ix2word爲每個序號對應的字,word2ix爲每個字對應的序號,prefix_words爲師哥意境
print(''.join(result))
"""主方法"""
def test():
gen({'model_path': 'checkpoints/tang_199.pth', 'pickle_path': 'tang.npz', 'start_words': '機器學習書',
'prefix_words': '牀前明月光,疑是地上霜。', 'acrostic': True, 'nouse_gpu': False})
test()
獲取數據data.py:
# coding:utf-8
import os
import numpy as np
"""從二進制文件中獲取詩歌數據"""
def get_data(opt): #讀取二進制numpy文件
"""
opt 配置選項 Config對象
dict,每個字對應的序號,形如u'月'->100
ix2word: dict,每個序號對應的字,形如'100'->'月'
data: numpy數組,每一行是一首詩對應的字的下標
"""
if os.path.exists(opt.pickle_path): #判斷路徑是否存在,即tang.npz這個文件是否存在(npz爲二進制文件)
data = np.load(opt.pickle_path,allow_pickle=True) #讀取tang.npz中的數據存到data中
data, word2ix, ix2word = data['data'], data['word2ix'].item(), data['ix2word'].item() #分類獲取數據
return data, word2ix, ix2word
模型model.py:
# coding:utf-8
import torch
import torch.nn as nn
"""詩歌模型"""
class PoetryModel(nn.Module):
def __init__(self, vocab_size, embedding_dim, hidden_dim):
"""
vocab_size: 所有字的長度8293
embedding_dim: 詞嵌入的維度128
hidden_dim: 隱藏層向量維度,即隱藏層節點個數256
"""
super(PoetryModel, self).__init__()
self.hidden_dim = hidden_dim
self.embeddings = nn.Embedding(vocab_size, embedding_dim) #權重矩陣爲vocab_size*embedding_dim,即8293*128
self.lstm = nn.LSTM(embedding_dim, self.hidden_dim, num_layers=2) #第三個參數爲網絡層數
self.linear1 = nn.Linear(self.hidden_dim, vocab_size) #全連接層,生成一個詞彙表,詞彙表的數值是概率,代表這句話下一個位置這個單詞出現的概率
def forward(self, input, hidden=None):
seq_len, batch_size = input.size() #input是一個一個的數字,seq_len表示每一句話多長(有多少單詞),batcg_size表示一次處理幾個句子
if hidden is None: #一開始設置h_0和c_0都爲0
h_0 = input.data.new(2, batch_size, self.hidden_dim).fill_(0).float() #隱藏元,維度爲(2*batch_size*hidden_dim),全部填充爲0,這裏的shape爲(2,1,256)
c_0 = input.data.new(2, batch_size, self.hidden_dim).fill_(0).float()
else:
h_0, c_0 = hidden
embeds = self.embeddings(input) #(1,1,128)
output, hidden = self.lstm(embeds, (h_0, c_0)) #output爲(1,1,256),hidden中包含了同爲(1,1,256)的h_0和c_0
output = self.linear1(output.view(seq_len * batch_size, -1)) #全連接後變成(1,8293)
return output, hidden