
導言:網路無所不在
想像一下你在瀏覽社群媒體動態。你的朋友分享了昨晚聚餐的照片,你看到照片裡有幾位共同好友。同時,應用程式也建議了幾個「你可能認識的人」,其中有些人看起來的確眼熟。之後,你使用地圖應用程式尋找最快回家的路線,避開交通壅塞。晚上,你收到電影推薦,結果竟然非常符合你的喜好。
這些經驗之間的連結是什麼?是網路——由無形關係網構成的世界。這些網路在數學上表現為「圖」(graphs),也是人工智慧最令人振奮領域之一:圖神經網路(GNNs)的基礎。
什麼是圖?連結式數據的骨幹
在電腦科學中,「圖」並不是長條圖或折線圖,而是一種用來表示關係的數學結構。
想像夜空中的星座:星星是「節點」(或稱「頂點」),將它們連接起來的想像線則是「邊」。由節點與邊所構成的簡單結構,足以表示無數真實世界中的系統。
真實世界中的圖例:
社群網路、分子結構以及道路網路只是圖在我們生活中隨處可見的幾個例子:
社群網路:你的社交圈就是圖的一個完美範例。每個人都是一個節點,而彼此的友誼關係則是將節點連接起來的邊。當 Facebook 建議「你可能認識的人」時,系統即是在分析這個圖結構,找出與你有共同連結但尚未直接連接的節點(人)。
交通系統: 城市是節點,道路、航線或鐵路則是邊。這些邊通常具有一些屬性,例如道路的長度、車流密度或速限。當你的 GPS 計算最快路線時,它其實正在解決一個圖的問題。
生物網路: 在人體中,蛋白質會與其他蛋白質作用,形成複雜的生物途徑。科學家使用圖來呈現這些關係,以瞭解疾病與開發治療方式。每個蛋白質都是一個節點,彼此互動的關係則形成邊。
網際網路: 網站是節點,超連結是邊。Google 最初的 PageRank 演算法就是透過分析這個龐大圖結構來為網頁排名。
知識圖: 維基百科的文章是節點,而連結則形成了邊。這些知識圖能協助組織人類資訊,也為許多 AI 助手提供支援。
神經網路:學習的機器
在探討圖神經網路之前,讓我們先瞭解一般的神經網路。
想像你正在教一個孩子辨識水果。一開始,他們會分別觀察顏色、形狀與大小。隨著練習,他們逐漸學會哪些特徵的組合能區分蘋果與橘子,也就養成了一種不需明確規則的直覺理解。
神經網路的學習方式與此類似:
輸入層: 接收原始資料(例如影像的像素值)。
隱藏層: 以愈來愈抽象的方式轉化並組合資訊。
輸出層: 產生最後的預測或分類結果。
「學習」的過程在於調整神經元之間連結的強度。這些連結最初是隨機的,之後會透過大量範例逐漸改變,強化能導向正確答案的連結,並削弱那些導致錯誤的連結。
舉例來說,神經網路在學習辨識貓的過程中,可能會形成偵測貓鬍鬚、尖耳朵或毛皮花紋的神經元——並不是因為我們告訴它要找這些特徵,而是它在學習過程中發現這些特徵對於完成任務很有幫助。
圖之挑戰:傳統神經網路為何難以應對
傳統神經網路非常擅長處理結構良好的資料,如影像(像素網格)或文字(單詞序列)。然而,圖卻帶來了獨特的挑戰:
可變的大小與結構: 社群網路可能包含數百萬使用者,且擁有複雜的連結模式。一個分子可能只有幾個原子,也可能上千個;但傳統神經網路通常假設輸入的大小是固定的。
沒有自然順序:在影像中,像素可自左上到右下有固定的順序;文字有從頭到尾的排列。但圖中的節點並沒有內建的順序。
關係資訊: 在圖中,連結本身通常比各個節點更重要。你認識誰,往往比你個人的細節更能揭示你的資訊。
以推薦系統為例,重點不只是你看過哪些電影(節點屬性),更重要的是相似觀眾間的偏好模式(圖結構)。傳統神經網路難以有效利用這些關鍵的關係資訊。
圖神經網路登場:從關係中學習
圖神經網路(GNNs)是一種專門針對圖結構資料設計的神經網路,它能將關係資訊納入學習過程
想像你參加一場雞尾酒派對,每個人一開始都擁有自己的一些資訊。在整個夜晚中,人們會與身邊的人交談,分享並收集資訊。到夜晚結束時,你所得到的認知已不只來自你直接對話的人,而是透過整個房間的訊息傳遞所累積的成果。
這基本上就是 GNNs 的運作方式。
訊息傳遞框架:GNNs 如何學習
大部分 GNNs 的核心機制稱為「訊息傳遞」,它包含以下幾個階段:
1. 節點初始化
每個節點都從一些初始特徵開始。這些特徵可能是:
- 社群網路中個人的資料資訊
- 分子中原子的化學性質
- 路口的交通狀況
- 推薦系統中使用者或產品的特徵
例如,在電影推薦的圖中,「使用者」節點可能包含人口統計資訊,而「電影」節點可能包含類型與上映年份等資訊。
2. 訊息建立與交換
節點會依據當前資訊建立「訊息」,並將它們傳送給相鄰節點。
想像一個犯罪調查板,上面有嫌疑人的照片,並用線連結代表彼此之間的關係。每位嫌疑人(節點)都有初始資訊(不在場證明、動機等)。在訊息傳遞過程中,嫌疑人會將自己的資訊「告訴」與其相連的嫌疑人,可能進而揭露「所有這些嫌疑人都曾在同一地點」的模式。
在交通網路中,一個路口可能將自身的壅塞程度「告訴」相鄰的路口,讓整體網路能逐漸掌握交通流動的模式。
3. 聚合
每個節點會收集來自相鄰節點的訊息並加以整合。由於圖節點並沒有固定的順序,因此這種聚合必須與順序無關,常見做法是透過加總、平均或取最大值等方式。
在派對的比喻中,這就像你在腦海中統整所有交談內容,擷取關鍵見解,而不是逐字記住所有對話。
在社群網路中,這可能是使用者節點從所有朋友節點收集資訊,進而理解這些朋友彼此之間的興趣或行為模式。
4. 更新
每個節點會依據當前狀態與聚合後的訊息來更新其表徵。
在派對的比喻中,這就好比你根據新得到的資訊來更新你對事物的理解。
在蛋白質交互作用網路中,一個蛋白質節點可能會更新它的表徵,不只顯示它本身的性質,也包含它與鄰近蛋白質的互動方式,進而揭示它在生物過程中的功能角色。
5. 重複
將步驟 2-4 重複多次,使資訊能在整個圖中傳遞。在每一輪結束後,節點就能獲取更廣範圍的鄰居資訊。
第一輪後,節點只能掌握直接相鄰的節點資訊;第二輪後,能掌握鄰居的鄰居;經過 K 輪後,理論上就能收到距離自己 K 步之內所有節點的資訊。
在科學論文的引文網路中,這讓一篇論文不僅能整合它直接引用的論文資訊,也能運用該領域更廣泛的研究基礎。
import torch
import torch.nn as nn
import torch.nn.functional as F
# Ensure you have torch_geometric installed: pip install torch_geometric
try:
from torch_geometric.nn import GCNConv
from torch_geometric.datasets import Planetoid
from torch_geometric.transforms import NormalizeFeatures
except ImportError:
print("PyTorch Geometric not found. Please install it: pip install torch_geometric")
# You might also need to install dependencies based on your PyTorch+CUDA version
# See: https://pytorch-geometric.readthedocs.io/en/latest/install/installation.html
exit()
# --- 1. Load Dataset ---
try:
# Load a standard citation network dataset (Cora)
# NormalizeFeatures standardizes node features to have zero mean and unit variance
dataset = Planetoid(root='./data/Cora', name='Cora', transform=NormalizeFeatures())
data = dataset[0] # Get the single graph object from the dataset
except Exception as e:
print(f"Error loading dataset: {e}")
print("Please ensure the dataset can be downloaded or accessed.")
exit()
# --- 2. Print Dataset Information ---
print(f'Dataset: {dataset}:')
print('===================')
print(f'Number of graphs: {len(dataset)}')
print(f'Number of nodes: {data.num_nodes}') # Use data.num_nodes
print(f'Number of node features: {dataset.num_node_features}') # Use dataset.num_node_features
print(f'Number of edge features: {dataset.num_edge_features}') # Use dataset.num_edge_features (usually 0 for Cora)
print(f'Number of classes: {dataset.num_classes}')
print(f'Number of edges: {data.num_edges}') # Use data.num_edges
print(f'Training nodes: {data.train_mask.sum().item()}') # Show count of training nodes
print(f'Validation nodes: {data.val_mask.sum().item()}') # Show count of validation nodes
print(f'Test nodes: {data.test_mask.sum().item()}') # Show count of test nodes
print(f'Graph has isolated nodes: {data.has_isolated_nodes()}')
print(f'Graph has self-loops: {data.has_self_loops()}')
print(f'Graph is undirected: {data.is_undirected()}')
print('=============================================================')
# --- 3. Define the GCN Model ---
class GCN(nn.Module):
"""
A simple two-layer Graph Convolutional Network.
"""
def __init__(self, in_channels, hidden_channels, out_channels):
super(GCN, self).__init__()
torch.manual_seed(12345) # for reproducibility
# First Graph Convolutional layer: maps node features to hidden dimensions
self.conv1 = GCNConv(in_channels, hidden_channels)
# Second Graph Convolutional layer: maps hidden dimensions to output classes
self.conv2 = GCNConv(hidden_channels, out_channels)
def forward(self, x, edge_index):
"""
Defines the forward pass of the GCN.
Args:
x (Tensor): Node feature matrix [num_nodes, in_channels]
edge_index (LongTensor): Graph connectivity in COO format [2, num_edges]
Returns:
Tensor: Log-softmax probabilities for each node [num_nodes, out_channels]
"""
# --- Layer 1 ---
# Apply graph convolution
x = self.conv1(x, edge_index)
# Apply ReLU activation function
x = F.relu(x)
# Apply dropout for regularization (only during training)
x = F.dropout(x, p=0.5, training=self.training)
# --- Layer 2 ---
# Apply second graph convolution
x = self.conv2(x, edge_index)
# --- Output ---
# Apply log_softmax for classification (works well with NLLLoss)
return F.log_softmax(x, dim=1)
# --- 4. Initialize Model and Optimizer ---
model = GCN(in_channels=dataset.num_node_features,
hidden_channels=16, # A common choice for hidden layer size
out_channels=dataset.num_classes)
print("nModel Architecture:")
print(model)
print('=============================================================')
# Optimizer: Adam is a popular choice
# weight_decay adds L2 regularization
optimizer = torch.optim.Adam(model.parameters(), lr=0.01, weight_decay=5e-4)
# --- 5. Training Function ---
def train():
"""
Performs a single training step.
"""
model.train() # Set model to training mode (enables dropout)
optimizer.zero_grad() # Clear gradients from previous iteration
# Perform forward pass
out = model(data.x, data.edge_index)
# Calculate loss using Negative Log Likelihood Loss
# Only calculate loss on the nodes in the training set (using train_mask)
loss = F.nll_loss(out[data.train_mask], data.y[data.train_mask])
# Perform backward pass: compute gradients
loss.backward()
# Update model parameters
optimizer.step()
return loss.item() # Return the loss value for this step
# --- 6. Testing Function ---
def test(mask):
"""
Evaluates the model on a given node mask (train, val, or test).
Args:
mask (BoolTensor): Mask indicating which nodes to evaluate.
Returns:
float: Accuracy on the specified node set.
"""
model.eval() # Set model to evaluation mode (disables dropout)
with torch.no_grad(): # Disable gradient calculation for efficiency
out = model(data.x, data.edge_index)
# Get predictions by finding the class with the highest log-probability
pred = out.argmax(dim=1)
# Compare predictions with true labels for the nodes in the mask
correct = pred[mask] == data.y[mask]
# Calculate accuracy
acc = int(correct.sum()) / int(mask.sum())
return acc
# --- 7. Training Loop ---
print("Starting training...")
for epoch in range(1, 201):
loss = train()
if epoch % 10 == 0:
# Evaluate on training, validation, and test sets
train_acc = test(data.train_mask)
val_acc = test(data.val_mask) # Also test on validation set
test_acc = test(data.test_mask)
print(f'Epoch: {epoch:03d}, Loss: {loss:.4f}, Train Acc: {train_acc:.4f}, Val Acc: {val_acc:.4f}, Test Acc: {test_acc:.4f}')
print("Training finished.")
print('=============================================================')
# --- 8. Final Evaluation ---
final_train_acc = test(data.train_mask)
final_val_acc = test(data.val_mask)
final_test_acc = test(data.test_mask)
print(f'Final Train Accuracy: {final_train_acc:.4f}')
print(f'Final Validation Accuracy: {final_val_acc:.4f}')
print(f'Final Test Accuracy: {final_test_acc:.4f}')
print('=============================================================')
# --- 9. Examine Node Embeddings (Optional) ---
print("nExamining node embeddings...")
model.eval() # Ensure model is in eval mode
with torch.no_grad():
# --- Get embeddings after the first layer (before dropout and second layer) ---
# Re-run the first part of the forward pass to get intermediate embeddings
hidden_embeddings = model.conv1(data.x, data.edge_index)
hidden_embeddings = F.relu(hidden_embeddings) # Apply activation
print(f"nShape of hidden embeddings: {hidden_embeddings.shape}") # [num_nodes, hidden_channels]
# Look at the embedding of the first few nodes
print("nSample node embeddings after first GCN layer + ReLU:")
print(hidden_embeddings[:3])
# --- Analyze similarity between connected nodes ---
if data.num_edges > 0:
# Find a pair of connected nodes (using the first edge in the list)
edge_index_sample = data.edge_index[:, 0] # Take the first edge [source_node, target_node]
node1_idx = edge_index_sample[0].item()
node2_idx = edge_index_sample[1].item()
print(f"nComparing embeddings for connected nodes {node1_idx} and {node2_idx}:")
# Get their embeddings from the hidden layer
emb1 = hidden_embeddings[node1_idx]
emb2 = hidden_embeddings[node2_idx]
# Calculate Cosine Similarity
# unsqueeze(0) adds a batch dimension, required by cosine_similarity
similarity = F.cosine_similarity(emb1.unsqueeze(0), emb2.unsqueeze(0))
print(f"Embedding Node {node1_idx}: {emb1.numpy().round(2)}") # Print rounded numpy array
print(f"Embedding Node {node2_idx}: {emb2.numpy().round(2)}")
print(f"Cosine similarity: {similarity.item():.4f}")
# --- Analyze similarity between potentially unconnected nodes (Optional) ---
# Find two nodes that are likely not connected (e.g., node 0 and node 100)
if data.num_nodes > 100:
node3_idx = 0
node4_idx = 100
print(f"nComparing embeddings for potentially unconnected nodes {node3_idx} and {node4_idx}:")
emb3 = hidden_embeddings[node3_idx]
emb4 = hidden_embeddings[node4_idx]
similarity_unconnected = F.cosine_similarity(emb3.unsqueeze(0), emb4.unsqueeze(0))
print(f"Embedding Node {node3_idx}: {emb3.numpy().round(2)}")
print(f"Embedding Node {node4_idx}: {emb4.numpy().round(2)}")
print(f"Cosine similarity: {similarity_unconnected.item():.4f}")
else:
print("nGraph has no edges, cannot compare connected node embeddings.")
print("nAnalysis complete.")
GNN 架構:針對不同任務的各種形式
如同傳統神經網路有多種形式(例如影像的 CNNs、序列的 RNNs),GNN 也有不同的架構:
圖卷積網路(GCNs): 類似於用於影像的 CNNs,GCN 會對圖資料應用卷積運算,特別適用於節點分類任務,例如在引文網路中判斷一篇科學論文屬於哪一類別。
圖注意力網路(GATs): 讓節點能夠對其鄰居節點分配不同程度的「注意力」。在社群網路中,你可能比起一般認識的人,更在意親密朋友的狀況。GATs 則能自動學習這些注意力權重。
GraphSAGE: 這種架構會抽樣固定數量的鄰居,藉此在面對超大型圖時維持運算效率。就像你會集中注意力在最相關的聯絡人身上,而不是嘗試追蹤你認識的所有人一樣。
圖自編碼器(Graph Autoencoders): 透過學習壓縮式的圖表徵來應用於連結預測(例如判斷兩個使用者是否會成為朋友)或社群偵測等任務。
實際應用:GNNs 的現實運用
藥物研發
製藥產業在藥物研發上投資了鉅額資金,但大部分候選藥物在臨床試驗階段都會失敗。GNNs 正在革新這個過程:
分子性質預測: 分子天生就能以圖形式表示(原子是節點、化學鍵是邊)。GNNs 可以用來預測溶解度或毒性等性質,而無需昂貴的實驗。Atomwise 與 DeepChem 等公司運用 GNNs 快速篩選數百萬潛在藥物化合物。
蛋白質結構預測: 理解蛋白質如何折疊能幫助科學家設計可與其作用的藥物。DeepMind 的 AlphaFold 2 雖不純粹是 GNN,但融入了基於圖的推理,在蛋白質結構預測上取得突破性成果。
以 COVID-19 的治療藥物開發為例,研究人員可以運用 GNNs 來找出可能與病毒關鍵蛋白質結合的分子,大幅縮小需要在實驗室中測試的候選名單。
社群網路分析
好友推薦: 當 LinkedIn 或 Facebook 建議你新增聯絡人時,常常使用的是基於圖的演算法。GNNs 能夠捕捉細微的模式——也許你更傾向與在特定規模公司工作的同行業人士建立連結。
假帳號偵測: 透過分析連結與互動模式,GNNs 能分辨可疑的帳號。一般使用者通常會隨時間有機成長地累積連結,但假帳號的連結模式常常非常特殊。
資訊擴散: 在公共衛生危機期間,瞭解資訊(與錯誤資訊)如何傳播至關重要。GNNs 能模擬這些擴散過程,協助平台識別並限制有害內容的傳播。
例如,在 COVID-19 大流行期間,GNNs 可以分析疫苗資訊在不同社群中的傳播方式,找出關鍵影響者與潛在的資訊落差。
交通預測與優化
交通預測: 洛杉磯、北京等城市使用基於 GNN 的系統來預測交通狀況。每個路段都被視為節點,具有當前車速、時間與天氣等特徵。GNN 能學習道路網路中不同區域之間的複雜依存關係。
共乘服務優化: Uber 與 Lyft 等公司使用基於圖的模型,以更有效率地為乘客與司機配對。GNNs 能協助預測整個城市的需求,並考量各種事件、天氣與歷史趨勢。
想像有一個系統能根據即時交通變化重新規劃配送車輛的路線,或依據預測的乘客流量來調整大眾運輸班次。
詐騙偵測
金融詐騙: 銀行利用 GNNs 來偵測可疑的交易模式。透過將使用者與交易表示為圖,許多不尋常的模式會變得更明顯,例如以循環方式轉帳的帳戶群組,或是異常集中的交易群。
保險詐騙: 保險理賠往往牽涉到個人、醫療機構與服務提供者之間的網路。GNNs 能偵測可疑的模式,例如經常申報不尋常醫療組合的機構,或有可疑關係的申請人群組。
舉例而言,GNN 可能會標示出一個可疑模式:數起車禍都涉及相同的一小群目擊者、駕駛以及維修廠——若只看單筆理賠申請,很可能會忽略這樣的詐騙集團。
推薦系統
電商推薦: Amazon 的「購買此商品的顧客也買了…」功能可以透過 GNNs 進一步強化,捕捉產品與消費者行為之間更複雜的關係。
內容推薦: Netflix 與 Spotify 等串流服務會構建豐富的圖結構,連結使用者、內容、藝術家、類型以及觀看/收聽記錄等。GNNs 能找出細微的偏好——或許你偏好科幻電影,但特別喜歡有強大女性角色且具哲學思維的作品。
跨領域推薦: GNNs 在整合不同種類項目的資訊方面表現優異。即使沒有使用者的跨領域反饋,系統也可能發現喜歡某些 podcast 的使用者也很可能喜歡特定書籍的關聯性。
想像一個串流平台發現你在週末傾向觀看科幻影集,而在平日晚間則喜歡看紀錄片,並依此調整推薦內容——不只知道你喜歡什麼,也知道你在什麼時候喜歡什麼。
最短路徑的突破:通用問題的解決方式
近期,AI 研究人員在神經網路領域取得重大進展,開發出能在各種不同規模的圖上解最短路徑問題的圖神經網路(GNNs)。這項突破對「演算法對齊」而言至關重要,展示了 AI 能學習以類似傳統演算法的方式進行系統化推理,而不僅只依賴模式辨識。
之所以令人驚豔,是因為:
傳統演算法 vs. 學習: 最短路徑問題早已有像 Dijkstra 或 Bellman-Ford 這樣的高效演算法。然而,研究人員證明 GNN 可以透過範例有效學習並實現 Bellman-Ford 演算法,而無需明確編程。
規模泛化: 大多數神經網路在面對比其訓練範例大很多的輸入時都會失效,這就像一個學生只能做兩位數乘法,卻無法處理五位數乘法。然而,研究人員的 GNN 能從小型圖學習,然後在任意大型的圖上解最短路徑問題。
演算法對齊: 其中的關鍵概念是「演算法對齊」——透過對 GNN 的結構進行設計,使其能自然地表達 Bellman-Ford 演算法的運作原理,再結合稀疏化正則化(鼓勵大部分權重為零),就能迫使網路「發現」這個真正的演算法。
這就好比你只用幾條街的小鎮教某人導航原理,卻發現他能完美掌握世界上任何一座城市的路線——即使是從未見過的超大型都市。這正是研究人員所取得的成果。
實務考量:使用 GNNs
如果你考慮在專案中使用 GNNs,以下是一些實務上的考量:
資料表示方式: 你會如何將問題表示為圖?節點、邊,以及它們的特徵是什麼?這些基本決策會影響之後的一切。
運算資源: 在大型圖上使用 GNNs 可能需要大量運算資源。鄰居抽樣或以叢集為基礎的方法能協助降低這方面的負擔。
特徵工程: 儘管 GNNs 會從圖結構中學習,提供更具資訊性的節點與邊特徵,仍能大幅提升效能。
評估方式: 你會如何衡量成效?使用節點分類的準確率、連結預測的效能,或是整個圖層級的分類?選擇的評估指標應該與你的問題目標一致。
可解釋性: 你能理解 GNN 為何做出特定預測嗎?對醫療或金融等關鍵應用而言,可解釋性可能相當重要。
挑戰與未來方向
儘管 GNNs 強大,仍面臨下列挑戰:
表達力的限制: 部分 GNNs 難以區分特定的圖結構。研究人員正在開發更具表達力的架構來克服這個問題。
動態圖: 許多真實世界的圖會隨著時間變化——好友關係可能新增或刪除,道路狀況也會改變。如何讓 GNNs 適應動態環境仍是活躍的研究領域。
可擴充性: 具有數百萬或數十億節點的超大圖在運算上構成嚴峻挑戰。工業應用通常需要專門的實作。
異質圖: 真實世界的圖通常包含不同類型的節點與邊。例如,一個知識圖可能包含人物、地點、事件和概念等多種類型節點,並透過不同關係類型連結。如何有效處理這種異質性仍是持續發展的研究領域。
未來的可能方向包含:
自我監督式學習: 不依賴標記資料便能學習有用的圖表徵,將能大幅拓展 GNN 的應用範圍。
圖生成: 建構符合特定需求且有效的全新圖,在藥物開發、材料科學以及合成資料產生等領域都有應用前景。
與其他方法結合: 將 GNNs 與強化學習或 transformer 等技術整合能帶來全新的可能性。
開始探索 GNNs
如果你對 GNNs 感興趣並想進一步探索:
打好基礎: 在研究 GNNs 之前,先確保自己對一般神經網路有紮實的理解。
由淺入深: 先從基礎的圖問題和小型資料集開始,再逐漸擴大規模。
使用現有函式庫: PyTorch Geometric、Deep Graph Library (DGL) 以及 Spektral 等框架都提供了常見 GNN 架構的實作。
加入社群: 這個領域正在快速發展。多關注研究論文,加入線上社群,或參加類似 Kaggle 等平台上的競賽。
總結:迎向更緊密連結的未來
圖神經網路代表了機器學習的一次根本轉變——從分析單獨的實體,到在脈絡中理解這些實體。它們體認到,在這個互聯的世界裡,關係往往比單一屬性更重要。
隨著世界越來越互聯化——從全球供應鏈到數位社群網路再到物聯網——GNNs 為理解並優化這些複雜系統提供了強而有力的方法。
在學習演算法(例如最短路徑查找)方面的突破,暗示著更大的發展潛力:神經網路能學習一般性的演算法原則,而不僅只是統計模式。這或許能讓 AI 結合神經網路的適應力與演算法的可靠性與泛化能力。
無論你是推動前沿研究的科學家、構建應用的開發者,還是對 AI 演進充滿好奇的一般人,圖神經網路都能提供一個令人著迷的視角——機器如何透過一次一次的連結,學習並航行在這個充滿網路關係的世界中。
✦延伸閱讀:AI Agent 是什麼?與 AI 助理、Chatbot 差異比較和應用場景解析
