在網路科學與自然語言處理的交集中,將非結構化的文本轉換為可量化分析的圖結構是一項基礎技術。傳統網路分析常依賴預先格式化的數據集,但現實世界的文本數據往往雜亂無章。因此,直接從程式碼層面逐一建構節點與邊界,成為更具彈性與控制力的解決方案。詞彙共現網路即為此方法的經典應用,它能將語言的線性序列轉化為複雜的關係網絡,從而揭示詞彙間的語義鄰近性與核心主題結構。本文所介紹的流程,不僅是技術實踐,更體現了從原始數據中提煉結構化洞見的分析思維,為後續的社群偵測、中心性分析等進階應用奠定基礎。

從程式碼建構網路:詞彙共現網路的實踐

在前幾節中,我們學習了如何從各種檔案格式(如邊界列表、鄰接列表、GEXF、JSON)讀取網路數據。然而,在實際應用中,數據的複雜性或不規則性有時會使得直接轉換檔案格式變得困難。此時,直接透過程式碼逐一添加節點和邊界來建構網路,便成為一種強大且靈活的解決方案。本節將以建構一個「詞彙共現網路」(Word Co-occurrence Network)為例,詳細闡述這一過程。

詞彙共現網路的概念

詞彙共現網路是一種用於分析文本數據中詞彙之間關係的網路模型。

  • 節點 (Nodes):代表文本中的單個詞彙。
  • 邊界 (Edges):表示兩個詞彙在同一文本單元(如句子、段落)中共同出現。
  • 邊界權重 (Edge Weights):通常表示兩個詞彙共同出現的頻率或次數。權重越高,表示詞彙之間的關聯越強。

本例將以瑪麗·雪萊的經典小說《科學怪人》(Frankenstein; or, The Modern Prometheus)的句子層級共現為基礎,構建詞彙共現網路。

建構網路的步驟與考量

建構詞彙共現網路的通用流程包括:

  1. 文本預處理

    • 分句:將整個文本分割成獨立的句子。
    • 分詞:將每個句子分割成單獨的詞彙。
    • 清理
      • 去除標點符號:移除句子中的標點符號,確保詞彙的準確性。
      • 轉換為小寫:將所有詞彙轉換為小寫,以避免大小寫差異導致的重複計算(例如,“The” 和 “the” 被視為同一個詞)。
      • 去除停用詞 (Stop Words):移除常見但對語義分析影響較小的詞彙,如冠詞(“the”, “a”)、代名詞(“he”, “she”)、連詞(“and”, “or”)等。這有助於聚焦於更具意義的詞彙。
  2. 網路建構

    • 初始化網路:創建一個空的 NetworkX 圖物件(例如,nx.Graph() 表示無向圖)。
    • 迭代處理
      • 對於句子中的每一個詞彙組合(例如,句子中的每兩個詞彙),在網路中創建或更新它們之間的邊界。
      • 如果邊界已存在,則增加其權重;如果不存在,則創建新邊界並設定初始權重。

程式碼實踐:co_occurrence_network 函數

以下是一個 Python 函數,演示了如何從文本字串建構詞彙共現網路:

import networkx as nx
import re # 引入正規表達式模組

# 定義停用詞集合 (可根據需要擴充)
stop_words = set(['the', 'of', 'and', 'a', 'to', 'in', 'is', 'it', 'that', 'this', 'i', 'you', 'he', 'she', 'they', 'we', 'for', 'on', 'with', 'as', 'by', 'at', 'from', 'or', 'be'])

def co_occurrence_network(text):
    """
    從文本字串建構詞彙共現網路。

    Args:
        text (str): 輸入的文本內容。

    Returns:
        nx.Graph: 建構好的詞彙共現網路圖。
    """
    G = nx.Graph() # 初始化一個無向圖

    # 1. 文本預處理:分句
    sentences = text.split('.') # 以句號分割句子

    for s in sentences:
        # 2. 文本預處理:清理句子 (去除標點、轉換小寫)
        # [^\w\n ]+ 匹配任何非字母數字、換行符或空格的字元
        clean = re.sub('[^\w\n ]+', '', s).lower()
        # 移除可能因正規表達式產生的多餘底線或空白
        clean = re.sub('_+', '', clean).strip()

        # 3. 文本預處理:分詞並過濾停用詞
        words = re.split('\s+', clean) # 以空白分割成詞彙列表
        # 過濾掉停用詞和空字串
        filtered_words = [word for word in words if word and word not in stop_words]

        # 4. 網路建構:為句子中的詞彙對創建邊界
        # 遍歷句子中的所有詞彙對
        for i in range(len(filtered_words)):
            for j in range(i + 1, len(filtered_words)):
                word1 = filtered_words[i]
                word2 = filtered_words[j]

                # 如果邊界已存在,增加權重;否則,創建新邊界並設定權重為 1
                if G.has_edge(word1, word2):
                    G[word1][word2]['weight'] += 1
                else:
                    G.add_edge(word1, word2, weight=1)

    return G

# --- 範例使用 ---
# 假設 'frankenstein_text' 包含小說的文本內容
# frankenstein_text = "..." # 實際使用時,請載入完整的文本

# 為了示範,我們使用一個簡短的範例文本
sample_text = """
It was the best of times, it was the worst of times.
The age of wisdom, it was the age of foolishness.
We had everything before us, we had nothing before us.
"""

# 建構網路
word_network = co_occurrence_network(sample_text)

# 輸出網路中的節點和邊界數量 (用於驗證)
print(f"節點數量: {word_network.number_of_nodes()}")
print(f"邊界數量: {word_network.number_of_edges()}")

# 輸出一些邊界的權重
print("\n部分邊界及其權重:")
for u, v, data in word_network.edges(data=True):
    if u in ['age', 'times', 'wisdom', 'foolishness', 'everything', 'nothing']: # 選擇一些關鍵詞
        print(f"({u}, {v}): weight={data['weight']}")
看圖說話:

此圖示總結了「從程式碼建構網路:詞彙共現網路的實踐」,旨在介紹如何透過程式碼來建構網路,並以詞彙共現網路為例進行說明。流程開頭首先聚焦於「從程式碼建構網路」,透過「分割」結構,詳細闡述了「詞彙共現網路的概念」(定義了「節點: 詞彙」、「邊界: 共同出現的關聯」、「權重: 共現頻率」,並指出其為「分析文本關係的工具」),接著探討了「建構網路的步驟與考量」(列出了「1. 文本預處理」中的「分句」、「分詞」、「清理」、「去除停用詞」等子步驟,以及「2. 網路建構」中的「初始化」、「迭代處理詞彙對」、「更新邊界權重」等關鍵操作),並展示了「程式碼實踐:co_occurrence_network 函數」(說明了「函數定義與參數」、「使用 re 模組進行文本處理」、「遍歷詞彙對創建邊界」,並提及了「範例使用與輸出驗證」)。最後,圖示以「總結與未來方向」作結,強調了「程式碼建構網路的靈活性」、「處理複雜、非結構化數據」,並預告了「下一章:進階網路分析與演算法」。

詞彙共現網路建構的細節與進階考量

在前一節中,我們介紹了建構詞彙共現網路的基本流程,包括文本預處理和使用 NetworkX 創建節點與邊界。本節將進一步深入探討 co_occurrence_network 函數的內部細節,特別是關於節點和邊界計數的處理,並提及了更專業的文本分詞工具。

函數內部細節:節點與邊界的計數更新

co_occurrence_network 函數在處理每個句子時,會對詞彙進行詳細的計數更新。這部分程式碼確保了網路能夠準確地反映詞彙的出現頻率以及它們之間的共現強度。

  1. 節點計數 (count)

    • 當處理一個詞彙 v 時,程式碼會嘗試訪問 G.nodes[v]['count']
    • 如果節點 v 已經存在於網路中,則將其計數 count 加一。
    • 如果節點 v 是第一次出現(KeyError),則會創建一個新節點 v,並將其計數初始化為 1。
    • 這確保了每個節點都記錄了它在整個文本中出現的總次數。
  2. 邊界計數 (count)

    • 對於句子中的每一對詞彙 (v, w)(其中 vw 不是停用詞,且不相等),程式碼會嘗試更新它們之間的邊界計數。
    • 如果邊界 (v, w) 已存在,則將其 count 屬性加一。
    • 如果邊界不存在,則創建一個新的邊界 (v, w),並將其 count 屬性設定為 1。
    • 這反映了詞彙 vw 在同一個句子中共同出現的次數。

程式碼片段解析:

# ... (前面已定義 stop_words 和 co_occurrence_network 函數) ...

# 假設 G 是一個 NetworkX 圖物件,並且已經處理了一些句子
# 這裡展示函數內部處理詞彙和邊界計數的部分

# 假設當前句子處理到詞彙 v
# 更新節點 v 的出現次數
try:
    G.nodes[v]['count'] += 1
except KeyError:
    # 如果節點 v 不存在,則添加節點並初始化計數
    G.add_node(v)
    G.nodes[v]['count'] = 1

# 遍歷句子中的所有詞彙 w,以創建或更新邊界 (v, w)
for w in words: # words 是當前句子清理後的詞彙列表

    # 跳過相同詞彙、停用詞或空字串
    if v == w or v in stop_words or w in stop_words or len(v) == 0 or len(w) == 0:
        continue

    # 更新邊界 (v, w) 的共現次數
    try:
        G.edges[v, w]['count'] += 1
    except KeyError:
        # 如果邊界 (v, w) 不存在,則創建新邊界並設定初始計數為 1
        G.add_edge(v, w, count=1)

# ... (函數繼續處理下一個詞彙或句子) ...

這種精確的計數機制是構建有意義的詞彙共現網路的基礎。權重值直接反映了詞彙之間的統計關聯性。

關於分詞 (Tokenization) 的說明

將文本分割成句子和詞彙的過程稱為「分詞」(Tokenization)。雖然我們在範例中使用了簡單的字串方法(如 split('.')re.split('\s+'))來實現分詞,但在實際的自然語言處理(NLP)應用中,這通常需要更為複雜和穩健的工具。

  • 專業分詞工具:像 spaCy、NLTK(Natural Language Toolkit)這樣的 NLP 函式庫提供了高度優化和準確的分詞器。它們能夠處理更複雜的語言結構、專有名詞、縮寫等,並能更有效地進行詞性標註、命名實體識別等任務。
  • 本書範圍:本書的重點在於網路分析,因此使用了簡化的分詞方法。對於深入的 NLP 研究,建議參考相關的 NLP 專門資源。

實際應用:載入《科學怪人》文本

有了 co_occurrence_network 函數後,載入並處理完整的《科學怪人》文本就變得相對直接。假設小說文本儲存在 data_dir / 'shelley1818' / 'frankenstein.txt' 路徑下。

from pathlib import Path
import networkx as nx
import re
import json # 引入 json 模組以備後用

# --- 前面定義的 stop_words 和 co_occurrence_network 函數 ---
stop_words = set(['the', 'of', 'and', 'a', 'to', 'in', 'is', 'it', 'that', 'this', 'i', 'you', 'he', 'she', 'they', 'we', 'for', 'on', 'with', 'as', 'by', 'at', 'from', 'or', 'be', 'was', 'were', 'had', 'have', 'has', 'will', 'would', 'should', 'could', 'do', 'does', 'did', 'but', 'so', 'if', 'then', 'than', 'what', 'when', 'where', 'why', 'how', 'all', 'any', 'some', 'such', 'no', 'nor', 'not', 'only', 'own', 'same', 'so', 'than', 'too', 'very', 's', 't', 'can', 'just', 'don', 'should', 'now'])

def co_occurrence_network(text):
    G = nx.Graph()
    sentences = text.split('.')
    for s in sentences:
        clean = re.sub('[^\w\n ]+', '', s).lower()
        clean = re.sub('_+', '', clean).strip()
        words = re.split('\s+', clean)
        filtered_words = [word for word in words if word and word not in stop_words]

        # 更新節點計數
        for v in filtered_words:
            try:
                G.nodes[v]['count'] += 1
            except KeyError:
                G.add_node(v)
                G.nodes[v]['count'] = 1

        # 更新邊界計數
        for i in range(len(filtered_words)):
            for j in range(i + 1, len(filtered_words)):
                v = filtered_words[i]
                w = filtered_words[j]
                # 確保 v 和 w 是有效的詞彙且不同
                if v and w and v != w:
                    if G.has_edge(v, w):
                        G.edges[v, w]['count'] += 1
                    else:
                        G.add_edge(v, w, count=1)
    return G
# --- 函數定義結束 ---

# 指定數據目錄和檔案路徑
data_dir = Path('.') / 'data' # 假設 'data' 目錄存在於當前路徑下
frankenstein_file = data_dir / 'shelley1818' / 'frankenstein.txt'

# 讀取《科學怪人》文本
try:
    with open(frankenstein_file, 'r', encoding='utf-8') as f:
        frankenstein_text = f.read()

    # 建構詞彙共現網路
    print("正在建構《科學怪人》的詞彙共現網路...")
    frankenstein_network = co_occurrence_network(frankenstein_text)
    print("網路建構完成!")

    # 輸出網路的基本統計資訊
    print(f"\n《科學怪人》詞彙共現網路統計:")
    print(f"節點數量: {frankenstein_network.number_of_nodes()}")
    print(f"邊界數量: {frankenstein_network.number_of_edges()}")

    # 顯示一些高權重的邊界 (表示詞彙共現頻繁)
    # 我們可以根據 'count' 屬性對邊界進行排序
    sorted_edges = sorted(frankenstein_network.edges(data=True), key=lambda x: x[2]['count'], reverse=True)

    print("\n共現頻率最高的 10 對詞彙:")
    for i in range(min(10, len(sorted_edges))):
        u, v, data = sorted_edges[i]
        print(f"- ({u}, {v}): 共現 {data['count']} 次")

except FileNotFoundError:
    print(f"錯誤:找不到檔案 '{frankenstein_file}'。請確保檔案存在於正確的路徑。")
except Exception as e:
    print(f"處理過程中發生錯誤: {e}")
看圖說話:

此圖示總結了「詞彙共現網路建構的細節與進階考量」,旨在深入探討建構詞彙共現網路的內部機制和相關工具。流程開頭首先聚焦於「詞彙共現網路建構的細節與進階考量」,透過「分割」結構,詳細闡述了「函數內部細節:節點與邊界的計數更新」(說明了「節點計數: 記錄詞彙總出現次數」、「邊界計數: 記錄詞彙對的共現頻率」,並指出「使用 try-except 處理節點/邊界是否存在」、「確保數據準確性」),接著探討了「關於分詞 (Tokenization) 的說明」(對比了「簡化分詞方法」與「專業工具: spaCy, NLTK」,並指出這是「NLP 領域的關鍵步驟」,同時說明「本書範疇為網路分析」),並展示了「實際應用:載入《科學怪人》文本」(提及了「定義 stop_words 集合」、「調用 co_occurrence_network 函數」、「處理完整文本數據」,並說明了「輸出網路統計資訊」、「分析高權重邊界」)。最後,圖示以「總結與未來方向」作結,強調了「函數實現細節的深入理解」、「實際數據應用與驗證」,並預告了「下一章:進階網路分析指標與社群偵測」。

檢視此程式化建構方法在處理非結構化數據的實踐效果,我們發現其價值遠不止於技術操作本身,更代表一種從「數據消費者」轉變為「模型建構者」的思維躍遷。與直接載入標準格式檔案相比,程式化建構賦予了分析者無與倫比的靈活性,以應對不規則、即時生成的數據流。然而,其挑戰也相當明確:分析的深度取決於前期「文本預處理」的嚴謹程度,例如分詞策略與停用詞的界定,直接決定了最終網路模型的品質與洞察力。此過程完美整合了自然語言處理的領域知識與網路科學的分析框架,將靜態文本轉化為動態的關係結構。

展望未來,隨著非結構化數據源的爆炸性增長,這種「即時建模」的能力將成為高階數據分析師的核心競爭力。我們預見,將特定領域知識(如語言學、社會學)編碼為模型建構規則的趨勢將愈發明顯,從而催生更具解釋力的客製化網路。

玄貓認為,對於致力於從原始數據中挖掘深層結構的專業人士而言,掌握這種從零建構模型的編程思維,是從數據處理者晉升為洞察創造者的關鍵一步,其戰略價值不容小覷。