代碼倉庫: https://github.com/brandonlyg/cute-dl
目標
上階段cute-dl已經可以構建基礎的RNN模型。但對文本相模型的支持不夠友好, 這個階段的目標是, 讓框架能夠友好地支持文本分類和本文生成任務。具體包括:
- 添加嵌入層, 為文本尋找高效的向量表示。
- 添加類別抽樣函數, 根據模型輸出的類別分布抽樣得到生成的文本。
- 使用imdb-review數據集驗證文本分類模型。
- 使用一個古詩數據集驗證文本生成模型。
這階段涉及到的代碼比較簡單因此接下來會重點描述RNN語言相關模型中涉及到的數學原理和工程方法。
數學原理
文本分類模型
可以把文本看成是一個詞的序列\(W=[w_1, w_2, ..., w_T]\), 在訓練數據集中每個文本屬於一個類別\(a_i\), \(a_i∈A\), 集合 \(A = \{ a_1, a_2, ..., a_k \}\) 是一個類別別集合. 分類模型要做的是給定一個文本W, 計算所有類別的后驗概率:
那么文本序列W的類別為:
即在給定文本的條件下, 具有最大后驗概率的類別就是文本序列W所屬的類別.
文本預測模型
設任意一個文本序列為\(W=[w_1,w_2,...,W_T]\), 任意一個詞\(w_i ∈ V\), V是所有詞匯的集合,也叫詞匯表, 這里需要強調的是\(w_i\)在V中是無序的, 但在W中是有序的, 文本預測的任務是, 計算任意一個詞\(w_i ∈ V\)在給定一個序列中的任意一個位置出現的概率:
文本預測輸出一個\(w_i ∈ V\)的分布列, 根據這個分布列從V中抽取一個詞即為預測結果。不同於分類任務,這里不是取概率最大的詞, 這里的預測結果是某個詞出現的在一個序列特定位置的個概率,只要概率不是0都有可能出現,所以要用抽樣的方法確定某次預測的結果。
詞的數字化表示
任意一條數據在送入模型之前都要表示為一個數字化的向量, 文本數據也不例外。一個文本可以看成詞的序列,因此只要把詞數字化了,文本自然也就數字化了。對於詞來說,最簡單的方式是用詞在詞匯表中的唯一ID來表示, ID需要遵守兩個最基本的規則:
- 每個詞的ID在詞匯表中必須是唯一的.
- 每個詞的ID一旦確定不能變化.
這種表示很難表達詞之間的關系, 例如: 在詞匯表中把"好"的ID指定為100, 如果希望ID能夠反映詞意的關系, 需要把"好"的近意詞: "善", "美", "良", "可以"編碼為98, 99, 101, 102. 目前為止這看起還行. 如果還希望ID能夠反映詞之間的語法關系, "好"前后經常出現的詞: "友", "人", "的", 這幾個詞的ID就很難選擇, 不論怎樣, 都會發現兩個詞它們在語義和語法上的關系都很遠,但ID卻很接近。這也說明了標量的表達能力很有限,無法表達多個維度的關系。為了能夠表達詞之間多個維度的的關系,多維向量是一個很好的選擇. 向量之間的夾大小衡量它們之間的關系:
對於兩個向量A, B使用它們的點積, 模的乘積就能得到夾角θ余弦值。當cos(θ)->1表示兩個向量的相似度高, cos(θ)->0 表示兩個向量是不相關的, cos(θ)->-1 表示兩個向量是相反的。
把詞的ID轉換成向量,最簡單的辦法是使用one-hot編碼, 這樣得到的向量有兩個問題:
- 任意兩個向量A,B, <A,B>=0, 夾角的余弦值cos(θ)=0, 不能表達詞之間的關系.
- 向量的維度等於詞匯表的大小, 而且是稀疏向量,這和導致模型有大量的參數,模型訓練過程的運算量也很大.
詞嵌入技術就是為解決詞表示的問題而提出的。詞嵌入把詞ID映射到一個合適維度的向量空間中, 在這個向量空間中為每個ID分配一個唯一的向量, 把這些向量當成參數看待, 在特定任務的模型中學習這些參數。當模型訓練完成后, 這些向量就是詞在這個特定任務中的一個合適的表示。詞嵌入向量的訓練步驟有:
- 收集訓練數據集中的詞匯, 構建詞匯表。
- 為詞匯表中的每個詞分配一個唯一的ID。假設詞匯表中的詞匯量是N, 詞ID的取值為:0,1,2,...,N-1, 對人任意一個0<ID<N-1, 必然存在ID-1, ID+1.
- 隨機初始化N個D維嵌入向量, 向量的索引為0,1,2,...,N-1. 這樣詞ID就成了向量的索引.
- 定義一個模型, 把嵌入向量作為模型的輸入層參與訓練.
- 訓練模型.
嵌入層實現
代碼: cutedl/rnn_layers.py, Embedding類.
初始化嵌入向量, 嵌入向量使用(-1, 1)區間均勻分布的隨機變量初始化:
'''
dims 嵌入向量維數
vocabulary_size 詞匯表大小
need_train 是否需要訓練嵌入向量
'''
def __init__(self, dims, vocabulary_size, need_train=True):
#初始化嵌入向量
initializer = self.weight_initializers['uniform']
self.__vecs = initializer((vocabulary_size, dims))
super().__init__()
self.__params = None
if need_train:
self.__params = []
self.__cur_params = None
self.__in_batch = None
初始化層參數時把所有的嵌入向量變成參與訓練的參數:
def init_params(self):
if self.__params is None:
return
voc_size, _ = self.__vecs.shape
for i in range(voc_size):
pname = 'weight_%d'%i
p = LayerParam(self.name, pname, self.__vecs[i])
self.__params.append(p)
向前傳播時, 把形狀為(m, t)的數據轉換成(m, t, n)形狀的數據, 其中t是序列長度, n是嵌入向量的維數.
'''
in_batch shape=(m, T)
return shape (m, T, dims)
'''
def forward(self, in_batch, training):
m,T = in_batch.shape
outshape = (m, T, self.outshape[-1])
out = np.zeros(outshape)
#得到每個序列的嵌入向量表示
for i in range(m):
out[i] = self.__vecs[in_batch[i]]
if training and self.__params is not None:
self.__in_batch = in_batch
return out
反向傳播時只關注當前批次使用到的向量, 注意同一個向量可能被多次使用, 需要累加同一個嵌入向量的梯度.
def backward(self, gradient):
if self.__params is None:
return
#pdb.set_trace()
in_batch = self.__in_batch
params = {}
m, T, _ = gradient.shape
for i in range(m):
for t in range(T):
grad = gradient[i, t]
idx = self.__in_batch[i, t]
#更新當前訓練批次的梯度
if idx not in params:
#當前批次第一次發現該嵌入向量
params[idx] = self.__params[idx]
params[idx].gradient = grad
else:
#累加當前批次梯度
params[idx].gradient += grad
self.__cur_params = list(params.values())
驗證
imdb-review數據集上的分類模型
代碼: examples/rnn/text_classify.py.
數據集下載地址: https://pan.baidu.com/s/13spS_Eac_j0uRvCVi7jaMw 密碼: ou26
數據集處理
數據集處理時有幾個需要注意的地方:
- imdb-review數據集由長度不同的文本構成, 送入模型的數據形狀為(m, t, n), 至少要求一個批次中的數據具有相同的序列長度, 因此在對數據進行分批時, 對數據按批次填充.
- 一般使用0為填充編碼. 在構建詞匯表時, 假設有v個詞匯, 詞匯的編碼為1,2,...,v.
- 由於對文本進行分詞, 編碼比較耗時。可以把編碼后的數據保存起來,作為數據集的預處理數據, 下次直接加載使用。
模型
def fit_gru():
print("fit gru")
model = Model([
rnn.Embedding(64, vocab_size+1),
wrapper.Bidirectional(rnn.GRU(64), rnn.GRU(64)),
nn.Filter(),
nn.Dense(64),
nn.Dropout(0.5),
nn.Dense(1, activation='linear')
])
model.assemble()
fit('gru', model)
訓練報告:
這個模型和tensorflow給出的模型略有差別, 少了一個RNN層wrapper.Bidirectional(rnn.GRU(32), rnn.GRU(32)), 這個模型經過16輪的訓練達到了tensorflow模型的水平.
文本生成模型
我自己收集了一個古由詩詞構成的小型數據集, 用來驗證文本生成模型. 代碼: examples/rnn/text_gen.py.
數據集下載地址: https://pan.baidu.com/s/14oY_wol0d9hE_9QK45IkzQ 密碼: 5f3c
模型定義:
def fit_gru():
vocab_size = vocab.size()
print("vocab size: ", vocab_size)
model = Model([
rnn.Embedding(256, vocab_size),
rnn.GRU(1024, stateful=True),
nn.Dense(1024),
nn.Dropout(0.5),
nn.Dense(vocab_size, activation='linear')
])
model.assemble()
fit("gru", model)
訓練報告:
生成七言詩:
def gen_text():
mpath = model_path+"gru"
model = Model.load(mpath)
print("loadding model finished")
outshape = (4, 7)
print("vocab size: ", vocab.size())
def do_gen(txt):
#編碼
#pdb.set_trace()
res = vocab.encode(sentence=txt)
m, n = outshape
for i in range(m*n - 1):
in_batch = np.array(res).reshape((1, -1))
preds = model.predict(in_batch)
#取最后一維的預測結果
preds = preds[:, -1]
outs = dlmath.categories_sample(preds, 1)
res.append(outs[0,0])
#pdb.set_trace()
txt = ""
for i in range(m):
txt = txt + ''.join(vocab.decode(res[i*n:(i+1)*n])) + "\n"
return txt
starts = ['雲', '故', '畫', '花']
for txt in starts:
model.reset()
res = do_gen(txt)
print(res)
生成的文本:
雲填纜首月悠覺
纜濯醉二隱隱白
湖杖雨遮雙雨鄉
焉秣都滄楓寓功
故民民時都人把
陳雨積存手菜破
好纜簾二龍藕卻
趣晚城矣中村桐
畫和春覺上蓋騎
滿楚事勝便京兵
肯霆唇恨朔上楊
志月隨肯八焜著
花夜維他客陳月
客到夜狗和悲布
關欲摻似瓦闊靈
山商過牆灘幽惘
是不是很像李商隱的風格?