本文將介紹基於基於文字的 GCN,使用 Pytorch 和基本的庫。GCN 模型是目前很新穎的半監督學習方法。
總的來說,它是將整個語料庫嵌入到一個以文件或者單詞為節點(有的帶標籤,有的不帶標籤)的圖中,各節點之間根據它們的關係存在帶有不同權重的邊。然後輸入帶有標籤的文件節點,訓練GCN模型,預測沒有標籤的文件。
本文選擇聖經作為語料庫,因為它是世界上最多閱讀並且有著非常豐富文字結構的書籍。聖經包括66本書(創世紀、出埃及記等)以及1189個章節。本監督學習任務是訓練一個語言模型能夠準確分類沒有標籤的章節屬於哪本書。(由於這個資料集的標籤是全部已知,所以本文選擇10%~20%的資料隱藏標籤作為測試集。)
為了解決這個問題,語言模型首先要學會區分各種書籍(比如《創世紀》更多的是談論亞當和夏娃,而《傳道書》講述了所羅門王的生平)。下面我們將展示文字 GCN 能夠很好的捕捉這些資訊。
語料庫
為了能夠讓 GCN 捕捉章節的上下文,我們建立了表示章節和單詞關係的圖,如上圖所示。節點由1189個章節和所有詞彙組成,這些節點之間是帶有權重的邊。權重 Aij 定義如下:
上式中,PMI 是滑動視窗多對共現單詞之間的逐點互資訊。#W 定義10個位元組的長度,#W(i) 是包含單詞 i 的滑動視窗數,#W(i,j) 是包含單詞 i 和 j 的滑動視窗數, #W 是語料庫中滑動視窗總數。TF-IDF 是一種加權技術,字詞的重要性隨著它在檔案中出現的次數成正比增加,但同時會隨著它在語料庫中出現的頻率成反比下降。直觀地說,具有高、正的 PMI 值的詞之間具有高語義相關性,相反,我們不會在具有負 PMI 的詞之間建立邊。總的來說,TF-IDF 加權文件和 word 之間的邊,捕獲文件內的上下文。PMI 加權詞彙之間的邊,可以跨文件捕獲上下文。
相比之下,不是基於圖的模型,這種跨文件的上下文資訊很難作為輸入特徵,並且模型必須標籤從頭開始學習它們。由於 GCN 可以提供文件之間的關聯關係,這些資訊於NLP 任務明確相關,所以可以預期 GCN 的表現將會更好。
計算 TF-IDF
計算 TF-IDF 相對簡單,我們知道數學公式,並且理解它的原理,只需要在1189 個文件中使用TfidfVectorizer 模組,並將結果儲存在 dataframe 中。為後面建立圖時文件-單詞之間的權重。程式碼如下:
### Tfidf
vectorizer = TfidfVectorizer(input="content", max_features=None, tokenizer=dummy_fun, preprocessor=dummy_fun)
vectorizer.fit(df_data["c"])
df_tfidf = vectorizer.transform(df_data["c"])
df_tfidf = df_tfidf.toarray()
vocab = vectorizer.get_feature_names()
vocab = np.array(vocab)
df_tfidf = pd.DataFrame(df_tfidf,columns=vocab)
計算詞彙間 PMI
計算詞彙之間的 PMI 要更復雜一些,首先我們需要在10個單詞長度的滑動視窗內找到單詞i,j的共現,以方塊矩陣的形式儲存在 dataframe 中,其中行和列表示詞彙表。然後使用之前的定義計算 PMI 。程式碼如下:
### PMI between words
window = 10 # sliding window size to calculate point-wise mutual information between words
names = vocab
occurrences = OrderedDict((name, OrderedDict((name, 0) for name in names)) for name in names)
# Find the co-occurrences:
no_windows = 0; print("calculating co-occurences")
for l in df_data["c"]:
for i in range(len(l)-window):
no_windows += 1
d = l[i:(i+window)];
dum = []
for x in range(len(d)):
for item in d[:x] + d[(x+1):]:
if item not in dum:
occurrences[d[x]][item] += 1; dum.append(item)
df_occurences = pd.DataFrame(occurrences, columns=occurrences.keys())
df_occurences = (df_occurences + df_occurences.transpose())/2
## symmetrize it as window size on both sides may not be same
del occurrences
### convert to PMI
p_i = df_occurences.sum(axis=0)/no_windows
p_ij = df_occurences/no_windows
del
df_occurences
for col in p_ij.columns:
p_ij[col] = p_ij[col]/p_i[col]
for row in p_ij.index:
p_ij.loc[row,:] = p_ij.loc[row,:]/p_i[row]
p_ij = p_ij + 1E-9
for col in p_ij.columns:
p_ij[col] = p_ij[col].apply(lambda x: math.log(x))
構圖
現在我們得到了所有邊的權重,可以開始構圖 G 了。我們使用 networkx 模組來構圖。這裡要提的是整個專案的繁重計算主要在於計算詞彙邊的權重,因為需要迭代所有可能成對的單片語合,大約有6500個單詞。(我們差不多花了兩天時間計算這個,程式碼如下)
def word_word_edges(p_ij):
dum = []; word_word = []; counter = 0
cols = list(p_ij.columns); cols = [str(w) for w in cols]
for w1 in cols:
for w2 in cols:
if (counter % 300000) == 0:
print("Current Count: %d; %s %s" % (counter, w1, w2))
if (w1 != w2) and ((w1,w2) not in dum) and (p_ij.loc[w1,w2] > 0):
word_word.append((w1,w2,{"weight":p_ij.loc[w1,w2]})); dum.append((w2,w1))
counter += 1
return word_word
### Build graph
G = nx.Graph()
G.add_nodes_from(df_tfidf.index)
## document nodes
G.add_nodes_from(vocab)
## word nodes
### build edges between document-word
pairs
document_word = [(doc,w,{"weight":df_tfidf.loc[doc,w]}) for doc in df_tfidf.index for w in df_tfidf.columns]
G.add_edges_from(document_word)
### build edges between word-word
pairs
word_word = word_word_edges(p_ij)
G.add_edges_from(word_word)
圖卷積神經網路
我們將在這裡使用兩層 GCN ,兩層 GCN 之後的複雜張量由下式給出:
這裡:
這裡的 A 是圖 G 的鄰接矩陣,D 是圖 G 的度矩陣。W0 和 W1 分別是 GCN 第一層和第二層可學習的卷積核權重,也是需要被訓練學習的。X 是輸入特徵矩陣,是與節點數相同的維度的對角方形矩陣,這意味著輸入是圖中每個節點的 one-hot 編碼。最後將輸出饋送到具有softmax 函式的層,用於書籍分類。
雙層 GCN 的Pytorch 程式碼如下:
class gcn(nn.Module):
def __init__(self, X_size, A_hat, bias=True): # X_size = num features
super(gcn, self).__init__()
self.A_hat = torch.tensor(A_hat, requires_grad=False).float()
self.weight = nn.parameter.Parameter(torch.FloatTensor(X_size, 330))
var = 2./(self.weight.size(1)+self.weight.size(0))
self.weight.data.normal_(0,var)
self.weight2 = nn.parameter.Parameter(torch.FloatTensor(330, 130))
var2 = 2./(self.weight2.size(1)+self.weight2.size(0))
self.weight2.data.normal_(0,var2)
if bias:
self.bias = nn.parameter.Parameter(torch.FloatTensor(330))
self.bias.data.normal_(0,var)
self.bias2 = nn.parameter.Parameter(torch.FloatTensor(130))
self.bias2.data.normal_(0,var2)
else:
self.register_parameter("bias", None)
self.fc1 = nn.Linear(130,66)
def forward(self, X): ### 2-layer GCN architecture
X = torch.mm(X, self.weight)
if self.bias is not None:
X = (X + self.bias)
X = F.relu(torch.mm(self.A_hat, X))
X = torch.mm(X, self.weight2)
if self.bias2 is not None:
X = (X + self.bias2)
X = F.relu(torch.mm(self.A_hat, X))
return self.fc1(X)
訓練
總共的 1189 個章節,我們掩蓋了111個標籤(10%左右)。由於1189 個章節的標籤分佈很不均勻(如下圖所示),所以一些分佈低的類別我們不會隱藏,以確保 GCN 可以學習到 66 個類別。
從上面的 loss vs epoch 圖中可以看到,訓練是很順利的,損失大約在 2000 epoch的時候達到飽和。
隨著訓練進行,訓練集準確度和測試集準確度同時增加,到 2000 epoch的時候,測試集準確度趨於飽和在 50% 左右。考慮到我們的類別有 66 個,假設模型以純粹的機會預測,基準精度為 1.5%,相比之下 50% 已經很不錯了。意思是,GCN 模型可以正確預測章節屬於哪本書的時候有 50% ,即使它之前並沒有見過這些章節。
GCN 可以捕捉文件內或者文件間的上下文資訊,但是分類錯誤的章節呢?這意味著 GCN 模型失敗嗎?
例子:
書籍《馬太福音》章節 27被模型預測為《路加》
查閱該章節的具體內容可以發現,該章節的內容和路加的部分內容很相似,都是在講耶穌被處死的事情。
書籍《以賽亞》章節12 被模型預測為《詩篇》
同樣的,《以賽亞》的 12 章節主要描述的是一些對上帝的讚美和歌頌,而這正是《詩篇》的全文主旨。
總結
專案地址
聖經資料集地址:
https://github.com/scrollmapper/bible_databases
圖神經網路的參照:
https://arxiv.org/abs/1809.05679
專案原始碼:
https://github.com/plkmo/Bible_Text_GCN