## QA 1：`nx.draw(G2)` 如何给每个节点标注对应的词汇？

### 问题

python

plt.figure(figsize=(15,14))
G 2 = nx.from_pandas_edgelist(df 2, "source", "target", edge_attr=True, create_using=nx.Graph())
print(G 2)
nx.draw(G 2)
plt.show()

想在每个节点上标注对应的词汇，怎么处理？

### 回答

`nx.draw()` 默认不显示节点标签，需要加 `with_labels=True`：

python

plt.figure(figsize=(15,14))
G 2 = nx.from_pandas_edgelist(df 2, "source", "target", edge_attr=True, create_using=nx.Graph())
print(G 2)
nx.draw(G 2, with_labels=True)
plt.show()

### 进一步自定义标签样式

python

nx.draw(
    G 2,
    with_labels=True,
    font_size=10,        # 字体大小
    font_color='red',    # 字体颜色
    font_weight='bold',  # 字体粗细
    node_color='lightblue',
    edge_color='gray',
    node_size=500
)

### 更复杂的标签控制：单独指定 labels 字典

python

labels = {node: node for node in G 2.nodes()}  # 每个节点用自身名称作为标签
nx.draw(G 2, labels=labels, with_labels=True)

如果想用节点的其他属性作为标签（比如 `df2` 里某个字段）：

python

labels = {node: G 2.nodes[node].get('word', node) for node in G 2.nodes()}
nx.draw(G 2, labels=labels, with_labels=True)

### 关键点

- `with_labels=True` 是最简单的方式。
    
- `labels` 参数可以传入字典，实现自定义标签。
    
- 字体、颜色、大小都可以通过 `font_*` 参数控制。
    
- 如果节点很多，标签会重叠，可以配合 `nx.spring_layout()` 或 `nx.kamada_kawai_layout()` 调整布局。
    

---

## QA 2：给一个数字 n，怎么随机到 1, 2, 3, ..., n（小于等于 n 的数字）？

### 问题

给定 n，想随机生成 1 到 n 之间的整数（包含 1 和 n）。

### 回答

Python 的 `random` 模块提供多种方式：

#### 方法一：`random.randint(1, n)`（最推荐）

python

import random
n = 10
random_number = random.randint(1, n)  # 生成 1 到 n 的随机整数（包含两端）
print(random_number)

#### 方法二：`random.randrange(1, n+1)`

python

random_number = random.randrange(1, n + 1)  # randrange 不包含上限，所以要 n+1

#### 方法三：`random.choice(range(1, n+1))`

python

random_number = random.choice(range(1, n + 1))

#### 方法四：`random.choices()`（可重复采样多个）

python

random_numbers = random.choices(range(1, n + 1), k=5)  # 一次生成 5 个

#### 方法五：`numpy.random.randint()`

python

import numpy as np
random_number = np.random.randint(1, n + 1)  # numpy 的 randint 也不含上限

### 对比总结

|方法|是否包含 n|推荐度|
|---|---|---|
| `random.randint(1, n)` |✅ 包含|⭐⭐⭐⭐⭐ 最推荐|
| `random.randrange(1, n+1)` |✅ 包含|⭐⭐⭐⭐|
| `random.choice(range(1, n+1))` |✅ 包含|⭐⭐⭐|
| `np.random.randint(1, n+1)` |✅ 包含|⭐⭐⭐⭐ 适合批量|

### 关键点

- `randint(a, b)` 是**闭区间** `[a, b]`。
    
- `randrange(a, b)` 是**左闭右开** `[a, b)`。
    
- `numpy.random.randint(a, b)` 也是**左闭右开**。
    

---

## QA 3：随机游走最长路径是 5，怎么还可以左边 4 个右边 4 个？

### 问题

python

gamma = 10          # 每个节点作为起始点生成随机游走序列个数
walk_length = 5     # 随机游走序列最大长度
random_walks = []
for n in tqdm(all_nodes):
    for i in range(gamma):
        random_walks.append(get_randomwalk(n, walk_length))
model = Word 2 Vec(
    vector_size=256,
    window=4,   # 窗口宽度为 4？？？？
    ...
)

疑问：`walk_length=5`（最长路径 5），但 `window=4`（左右各 4 个词），这不冲突吗？

### 回答

**完全不冲突**，因为这两个参数控制的是**不同阶段**。

#### 1. `walk_length=5` 的作用

- 控制**随机游走序列的长度**。
    
- 每条序列最多包含 5 个节点，例如：
    
    text
    
    [A, B, C, D, E]   # 长度=5
    [X, Y, Z]         # 长度=3（可能提前终止）
    
- 它决定了**输入数据的长度**。
    

#### 2. `window=4` 的作用

- 控制 **Word 2 Vec 训练时的上下文窗口**。
    
- 对每个中心词，左右各看最多 4 个词。
    
- 它决定了**每个目标词的上下文范围**。
    

#### 3. 为什么不冲突？

- `walk_length` 生成的是**原始序列**。
    
- `window` 决定从序列中**如何提取 (center, context) 训练对**。
    
- 序列长度是 5，窗口是 4，Word 2 Vec 仍然可以处理，只是对于靠近边界的中心词，实际上下文会少于 4 个。
    

#### 4. 具体例子

序列：`[A, B, C, D, E]`，`walk_length=5`，`window=4`

对每个中心词：

|中心词|左上下文|右上下文|实际上下文|
|---|---|---|---|
|A|无|B, C, D, E|[B, C, D, E]|
|B|A|C, D, E|[A, C, D, E]|
|C|A, B|D, E|[A, B, D, E]|
|D|A, B, C|E|[A, B, C, E]|
|E|A, B, C, D|无|[A, B, C, D]|

即使序列只有 5 个词，`window=4` 仍然可以对每个中心词取到尽可能多的上下文。

#### 5. 极端情况

- 如果 `walk_length=2`，序列 `[A, B]`，`window=4`：
    
    - A 的上下文只能是 `[B]`。
        
    - B 的上下文只能是 `[A]`。
        
    - Word 2 Vec 不会报错，只是窗口被截断。
        

#### 6. 类比理解

- `walk_length=5`：每次只读 5 行书。
    
- `window=4`：读每一行时，同时看它附近的 4 行来理解上下文。
    
- 即使一章只有 5 行，你仍然可以用"看附近 4 行"的方式去读。
    

#### 7. 调整建议

|参数|作用|一般取值范围|
|---|---|---|
| `walk_length` |序列长度|5–20（取决于图的直径）|
| `window` |上下文范围|2–10|

- 想捕获更长依赖 → 增大 `window`。
    
- 想生成更长序列 → 增大 `walk_length`。
    

### 关键结论

- `walk_length` 和 `window` **完全独立**，控制不同阶段。
    
- 二者不会冲突，即使 `walk_length < window` 也能正常运行。
    
- `walk_length` 提供数据，`window` 决定如何学习。
    

---

## QA 4：`window=4` 时，`[A,B,C,D,E]` 中 B 的上下文到底是什么？

### 问题

之前说 "对于 B，上下文可能是 [A, C, D]（窗口=2 时是 [A, C]）"，但序列 `[A,B,C,D,E]` 中，窗口为 2 的时候不应该是 `[A,C,D]` 吗？

### 回答

你的质疑是**对的**，之前的描述有误。正确规则如下：

#### 1. 窗口的严格定义

`window=w` 表示：从中心词向**左右各看最多 w 个词**（总窗口宽度最多 `2w`）。

#### 2. 序列 `[A,B,C,D,E]`，`window=2`

以 B（位置=1）为中心：

- 向左看 2 个：A（位置=0），再左边没有 → 左上下文 `[A]`
    
- 向右看 2 个：C（位置=2）、D（位置=3）→ 右上下文 `[C, D]`
    
- **实际上下文：`[A, C, D]` **
    

之前说 `[A, C]` 是 `window=1` 的情况，不是 `window=2`。

#### 3. 所有中心词的上下文（`window=2`）

|中心词|位置|左上下文|右上下文|实际上下文|
|---|---|---|---|---|
|A|0|无|B, C|[B, C]|
|B|1|A|C, D|[A, C, D]|
|C|2|A, B|D, E|[A, B, D, E]|
|D|3|B, C|E|[B, C, E]|
|E|4|C, D|无|[C, D]|

#### 4. 不同 window 的对比（中心词 B）

|window|B 的上下文|解释|
|---|---|---|
|1|[A, C]|左右各 1 个|
|2|[A, C, D]|左 1 个（A），右 2 个（C, D）|
|3|[A, C, D, E]|左 1 个（A），右 3 个（C, D, E）|
|4|[A, C, D, E]|左 1 个（A），右 3 个（C, D, E，E 之后没了）|

#### 5. 通用规则

对于中心词位置 `i`，序列长度 `L`，窗口 `w`：

- 左上下文：`max(0, i-w)` 到 `i-1`
    
- 右上下文：`i+1` 到 `min(L-1, i+w)`
    

#### 6. 代码验证

python

from gensim.models import Word 2 Vec
sentences = [["A", "B", "C", "D", "E"]]
model = Word 2 Vec(sentences, vector_size=10, window=2, sg=1, min_count=1)
print(model.wv.most_similar("B", topn=5))

输出中 A、C、D 的相似度较高，验证了 B 的上下文是 `[A, C, D]`。

### 关键结论

- `window=w` 是**单侧**最多看 w 个词。
    
- 实际上下文数量受序列边界限制。
    
- 之前说 `window=2` 时 B 的上下文是 `[A, C]` 是**错误的**，正确是 `[A, C, D]`。
    

---

## QA 5：`A = mat['network']`、`graph = sparse2graph(A)`、`labels_matrix = mat['group']` 是什么意思？

### 问题

python

A = mat['network']
graph = sparse 2 graph(A)
labels_matrix = mat['group']

这三行是什么意思？

### 回答

#### 1. `A = mat['network']`

- 从 `.mat` 文件中提取名为 `'network'` 的变量。
    
- 类型：SciPy 稀疏矩阵。
    
- 形状：10312 × 10312。
    
- 含义：图的**邻接矩阵**。
    
    - `A[i, j] = 1` 表示节点 i 和节点 j 之间有边。
        
    - 如果是无向图，矩阵是对称的。
        

#### 2. `graph = sparse2graph(A)`

- 把稀疏邻接矩阵转换成**图数据结构**（如 `networkx.Graph` 或字典）。
    
- `sparse2graph` 通常是自定义函数，可能实现如下：
    

python

import networkx as nx
def sparse 2 graph(sparse_matrix):
    G = nx.Graph()
    rows, cols = sparse_matrix.nonzero()
    for i, j in zip(rows, cols):
        G.add_edge(i, j)
    return G

- 结果：`graph` 是一个图对象，可用于随机游走、图分析。
    

#### 3. `labels_matrix = mat['group']`

- 从 `.mat` 文件中提取名为 `'group'` 的变量。
    
- 类型：SciPy 稀疏矩阵。
    
- 形状：10312 × 39。
    
- 含义：**节点的类别标签矩阵**（多标签分类）。
    
    - `labels_matrix[i, j] = 1` 表示节点 i 属于类别 j。
        
    - 一个节点可以属于多个类别。
        

#### 4. 三者关系

|变量|类型|形状|用途|
|---|---|---|---|
| `A` |稀疏矩阵|10312×10312|邻接矩阵，描述图结构|
| `graph` |networkx.Graph|10312 节点|图对象，用于随机游走|
| `labels_matrix` |稀疏矩阵|10312×39|节点类别标签，用于监督任务|

#### 5. 典型后续操作

**生成随机游走：**

python

walks = []
for _ in range(10):
    for node in graph.nodes():
        walks.append(nx.random_walk(graph, node, length=5))

**训练嵌入：**

python

from gensim.models import Word 2 Vec
model = Word 2 Vec(walks, vector_size=128, window=4, sg=1, hs=1)

**节点分类：**

python

from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
labels_dense = labels_matrix.toarray()
X = [model.wv[str(node)] for node in graph.nodes()]
X_train, X_test, y_train, y_test = train_test_split(X, labels_dense, test_size=0.2)
clf = LogisticRegression().fit(X_train, y_train)
print(clf.score(X_test, y_test))

#### 6. 常见问题

**Q：为什么 `labels_matrix` 是 10312×39？**  
A：10312 个节点，39 个类别，多标签分类。

**Q：`sparse2graph` 是哪里定义的？**  
A：可能是自定义函数，或来自某个库。需检查代码上下文。

**Q：如何验证数据加载正确？**

python

print(A.nnz)                    # 边的数量
print(labels_matrix.sum(axis=0))  # 每个类别的节点数

### 关键结论

- `A` 是邻接矩阵，`graph` 是图对象，`labels_matrix` 是标签矩阵。
    
- `network` 用于建图，`group` / `graph` 用于分类标签。
    
- 两者用途不同，不要混淆。
    

---

## QA 6：`graph` 是用来干嘛的？

### 问题

python

data = scipy.io.loadmat("blogcatalog.mat")
graph = data["graph"]          # 10312×39 的类别矩阵
network = data["network"]      # 10312×10312 的邻接矩阵
G = nx.from_scipy_sparse_matrix(network)
walks = []
for _ in range(10):
    for node in G.nodes():
        walks.append(nx.random_walk(G, node, length=5))
model = Word 2 Vec(walks, vector_size=128, window=4, sg=1, hs=1)

`graph` 是用来干嘛的？

### 回答

#### 1. `graph` 的真实用途

- `graph` 是 10312 × 39 的稀疏矩阵。
    
- 表示**节点与类别的关系**。
    
- `graph[i, j] = 1` 表示节点 i 属于类别 j。
    
- 它是**监督学习的标签**，用于节点分类任务。
    

#### 2. 你的代码中 `graph` 实际没被用到

- 建图用的是 `network`（邻接矩阵）。
    
- 随机游走用的是 `G`（由 `network` 构建）。
    
- `graph` 被加载后没有参与任何计算。
    

#### 3. 变量用途对比

|变量名|数据内容|用途|
|---|---|---|
| `graph` |10312×39 类别矩阵|节点分类标签（未使用）|
| `network` |10312×10312 邻接矩阵|构建图 G，生成随机游走|
| `G` |networkx.Graph|图拓扑结构|
| `walks` |随机游走序列|输入 Word 2 Vec|
| `model` |Word 2 Vec 模型|节点嵌入|

#### 4. 如果想用 `graph` 做分类

python

import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
labels_matrix = data["group"]  # 或 data["graph"]
labels_dense = labels_matrix.toarray()
embeddings = np.array([model.wv[str(node)] for node in G.nodes()])
X_train, X_test, y_train, y_test = train_test_split(embeddings, labels_dense, test_size=0.2)
clf = LogisticRegression()
clf.fit(X_train, y_train)
print("分类准确率:", clf.score(X_test, y_test))

#### 5. 为什么代码能跑但没用到 `graph`？

- DeepWalk 是**无监督图嵌入**，只需要邻接矩阵 `network`。
    
- `graph` 是额外信息，只有做分类任务才需要。
    
- 所以你的代码能正常运行，但 `graph` 被闲置了。
    

#### 6. 建议改名避免混淆

python

data = scipy.io.loadmat("blogcatalog.mat")
adj_matrix = data["network"]      # 邻接矩阵
labels_matrix = data["group"]     # 类别标签矩阵

### 关键结论

- `graph` 是类别标签矩阵，用于监督分类。
    
- 你的 DeepWalk 代码只用 `network`，`graph` 没用到。
    
- 想用 `graph` 需额外写分类代码。
    

---

## QA 7：随机游走序列、Word 2 Vec 训练，输入是什么，输出是什么，如何训练？

### 问题

python

walks = [
    ["1", "2", "4", "5", "4"],
    ["2", "4", "3", "1", "2"],
    ["3", "4", "5", "4", "3"],
    ...
]
model = Word 2 Vec(
    walks,
    vector_size=64,
    window=3,
    min_count=0,
    workers=2
)

输入是什么，输出是什么，如何训练？

### 回答

#### 1. 输入

**（1）随机游走序列 `walks` **

- 格式：`List[List[str]]`，每个子列表是一条节点序列。
    
- 示例：
    
    python
    
    walks = [
        ["1", "2", "4", "5", "4"],
        ["2", "4", "3", "1", "2"],
        ["3", "4", "5", "4", "3"],
        ...
    ]
    
- 生成方式：`graph.build_deepwalk_corpus()` 或 `nx.random_walk()`。
    
- 每条序列长度 = `walk_length`（如 5）。
    
- 序列数量 = `num_nodes × walks_per_node`（如 10312 × 10 = 103120）。
    

**（2）Word 2 Vec 参数**

|参数|值|作用|
|---|---|---|
| `vector_size` |64|嵌入维度|
| `window` |3|上下文窗口大小|
| `min_count` |0|不忽略任何节点|
| `workers` |2|并行线程数|

#### 2. 输出

**（1）训练好的 Word 2 Vec 模型**

- 可通过 `model.wv` 访问嵌入向量：
    
    python
    
    model.wv["1"]  # 返回节点 "1" 的 64 维向量
    

**（2）节点嵌入矩阵**

- 形状：`(num_nodes, 64)`
    
- 每行是一个节点的嵌入向量。
    

**（3）保存与加载**

python

model.save("node_embeddings.model")
loaded_model = Word 2 Vec.load("node_embeddings.model")

#### 3. 训练过程

**（1）滑动窗口生成训练样本**

对序列 `["1", "2", "4", "5", "4"]`，`window=3`：

- 中心词 "2" 的上下文：`["1", "4", "5"]`（左右各最多 3 个，受边界限制）。
    
- 中心词 "4" 的上下文：`["2", "5", "4"]`。
    

**（2）优化目标**

对每个 `(center, context)` 对：

- 最大化 `P(context | center)`。
    
- 使用**负采样**或**层次 Softmax** 加速。
    

**（3）参数更新**

- 通过梯度下降调整：
    
    - 中心词的嵌入向量（`syn0`）。
        
    - 上下文词的输出向量（`syn1` 或 `syn1neg`）。
        

#### 4. 完整代码示例

python

import networkx as nx
from gensim.models import Word 2 Vec
graph = nx.karate_club_graph()
def build_deepwalk_corpus(graph, walks_per_node=10, walk_length=5):
    walks = []
    for _ in range(walks_per_node):
        for node in graph.nodes():
            walk = nx.random_walk(graph, str(node), length=walk_length)
            walks.append(list(map(str, walk)))
    return walks
walks = build_deepwalk_corpus(graph, walks_per_node=10, walk_length=5)
model = Word 2 Vec(
    walks,
    vector_size=64,
    window=3,
    min_count=0,
    sg=1,       # Skip-gram
    hs=0,       # 负采样
    workers=2
)
print(model.wv["1"])
print(model.wv.most_similar("1"))

#### 5. 常见问题

**Q：为什么 `min_count=0`？**  
A：确保所有节点（包括孤立节点）都参与训练。若设为正数，低频节点会被忽略。

**Q：`window=3` 的实际上下文范围？**  
A：理论左右各 3 个，实际受序列边界限制。

**Q：如何验证训练效果？**

python

model.wv.most_similar("1")  # 查看与节点 "1" 最相似的节点

### 关键结论

- 输入：随机游走序列 + 超参数。
    
- 输出：嵌入矩阵 `(num_nodes, vector_size)`。
    
- 训练：滑动窗口 + Skip-gram + 负采样/层次 Softmax。
    

---

## QA 8：Skip-gram 的输入是 One-hot，输出是 Multi-hot 吗？

### 问题

序列 `["1", "2", "4", "5", "4"]`，`window=3`：

- 中心词 "2" 的上下文：`["1", "4", "5"]`。
    
- 输入的是 5 个，输出的是 5 个，输入的 2 为 1 其余为 0；真实值的 1、4、5 为 1 其余为 0？
    

### 回答

#### 1. Skip-gram 的输入输出

**输入**：中心词的 One-hot 编码（维度 = 词汇表大小）。

- 实际实现中**不显式构造 One-hot**，而是直接查表获取嵌入向量。
    

**输出**：模型预测的**上下文节点概率分布**（维度 = 词汇表大小）。

- 目标是让真实上下文节点的概率最大化。
    

#### 2. 你的例子

序列 `["1", "2", "4", "5", "4"]`，`window=3`：

- 中心词 "2"（位置=1）：
    
    - 左：`["1"]`
        
    - 右：`["4", "5"]`
        
    - 实际上下文：`["1", "4", "5"]`
        

**训练样本：**

- 输入：`"2"`
    
- 输出（目标）：`"1"`, `"4"`, `"5"`（多个正样本）
    

#### 3. 数学表示

**（1）输入（中心词）**

- 原始形式：One-hot 向量，如 `[0, 1, 0, 0, 0]`（假设词汇表 `["1","2","4","5"]`）。
    
- 实际实现：直接通过嵌入查找表获取中心词向量。
    
    python
    
    model.wv["2"]  # 直接返回 "2" 的嵌入向量
    

**（2）输出（上下文词）**

- 目标：输出一个概率分布，希望真实上下文词概率高。
    
- 例如，对中心词 "2"，希望 `P("1"|"2")`、`P("4"|"2")`、`P("5"|"2")` 高。
    
- 损失函数：使用**负采样**或**层次 Softmax**。
    

#### 4. 你的疑问澄清

**问题**：输入的 2 为 1 其余为 0；真实值的 1、4、5 为 1 其余为 0？

**回答**：

- **输入**：中心词 "2" 的 One-hot 编码确实是 `[0, 1, 0, 0, 0]`（但实际不显式构造）。
    
- **输出**：
    
    - **不是** Multi-hot 编码（即不是 `[1, 0, 1, 1, 0]`）。
        
    - 而是多个 `(center, context)` 对：
        
        - `("2", "1")`
            
        - `("2", "4")`
            
        - `("2", "5")`
            
    - 每个对单独训练（共享相同的中心词向量）。
        

#### 5. 具体训练步骤（中心词 "2"）

1. **获取中心词向量**：从嵌入矩阵查找 "2" 的向量 `v_2`（64 维）。
    
2. **计算上下文词概率**：
    
    - 对每个上下文词 `c ∈ ["1", "4", "5"]`：
        
        - 计算点积：`score = v_2 · u_c`（`u_c` 是词 c 的输出向量）。
            
        - 通过 Sigmoid 转换为概率：`P(c|"2") = σ(score)`。
            
3. **优化目标**：
    
    - 最大化 `P("1"|"2")`、`P("4"|"2")`、`P("5"|"2")`。
        
    - 同时最小化负样本的概率（如随机采样的 "3"）。
        

#### 6. 总结

|组件|表示方式|实际实现|
|---|---|---|
|输入|One-hot 编码（理论）|直接查找嵌入向量（实践）|
|输出|多个 (center, context) 对|对每个对单独计算概率并优化|
|训练|最大化正样本概率，最小化负样本概率|使用负采样或层次 Softmax 加速|

#### 7. 代码示例（模拟训练过程）

python

import numpy as np
vocab = {"1": 0, "2": 1, "4": 2, "5": 3}
vocab_size = len(vocab)
embed_dim = 64
input_embeddings = np.random.randn(vocab_size, embed_dim)
output_embeddings = np.random.randn(vocab_size, embed_dim)
center = "2"
contexts = ["1", "4", "5"]
negative_samples = ["3"]
learning_rate = 0.01
for c in contexts:
    input_vec = input_embeddings[vocab[center]]
    output_vec = output_embeddings[vocab[c]]
    gradient = (1 - sigmoid(np.dot(input_vec, output_vec))) * output_vec
    input_embeddings[vocab[center]] += learning_rate * gradient
    output_embeddings[vocab[c]] += learning_rate * gradient

### 关键结论

- 输入是中心词（隐式 One-hot），输出是多个上下文词（非 Multi-hot）。
    
- Word 2 Vec 通过滑动窗口生成多个 `(center, context)` 对，逐个优化。
    
- 你的理解中 "输入的 2 为 1 其余为 0" 是正确的（理论输入），但输出是**多个独立的二分类任务**，而非 Multi-hot 向量。
    

---

## QA 9：邻接矩阵中的节点是怎么从数字变成字符串的？

### 问题

`network` 是 10312×10312 的稀疏矩阵，节点是整数索引。它是怎么从数字变成字符串的？

### 回答

#### 1. 邻接矩阵的原始形式

- `network` 是 SciPy 稀疏矩阵。
    
- `network[i, j] = 1` 表示节点 i 和 j 之间有边。
    
- 节点索引是整数（0, 1, 2, ..., 10311）。
    

#### 2. 为什么需要转换为字符串？

- **Word 2 Vec 的输入要求**：Gensim 的 Word 2 Vec 默认要求输入序列中的元素是**字符串**。
    
- 如果直接输入整数（如 `[0, 1, 2]`），Word 2 Vec 会将其视为字符串 `"0"`, `"1"`, `"2"`，不影响功能但可能降低可读性。
    
- **最佳实践**：显式转换为字符串，避免后续混淆。
    

#### 3. 转换发生的关键步骤

**（1）构建图时（networkx）**

python

import networkx as nx
from scipy.sparse import csr_matrix
network = csr_matrix((data, (rows, cols)), shape=(10312, 10312))
G = nx.from_scipy_sparse_matrix(network)
G = nx.relabel_nodes(G, {i: str(i) for i in G.nodes()})  # 整数 → 字符串

**（2）生成随机游走序列时**

如果未重命名节点，需在生成游走序列时转换：

python

walks = []
for _ in range(10):
    for node in G.nodes():
        walk = nx.random_walk(G, node, length=5)
        walks.append([str(n) for n in walk])

#### 4. 完整代码示例

python

import networkx as nx
from scipy.io import loadmat
from gensim.models import Word 2 Vec
data = loadmat("blogcatalog.mat")
network = data["network"]
G = nx.from_scipy_sparse_matrix(network)
G = nx.relabel_nodes(G, {i: str(i) for i in G.nodes()})
walks = []
for _ in range(10):
    for node in G.nodes():
        walks.append(nx.random_walk(G, node, length=5))
model = Word 2 Vec(walks, vector_size=64, window=3, min_count=0, sg=1, workers=4)
print(model.wv["0"])

#### 5. 为什么不用整数直接训练？

- **功能上可行**：Word 2 Vec 不关心节点是字符串还是整数。
    
- **可读性**：字符串更直观。
    
- **避免潜在问题**：某些库版本可能对非字符串输入处理不一致。
    

#### 6. 常见问题

**Q：不转换会报错吗？**  
A：不会，但可能导致后续操作混淆（如 `model.wv[0]` 和 `model.wv["0"]` 可能不同）。

**Q：如何还原节点原始 ID？**  
A：训练后直接按字符串访问：

python

embedding = model.wv["123"]

**Q：为什么 networkx 不默认返回字符串？**  
A：图库通常保留原始节点类型，由用户决定是否转换。

### 关键结论

- 节点从整数变字符串通过 `nx.relabel_nodes` 或 `str(n)` 实现。
    
- Word 2 Vec 要求字符串 token，转换是推荐做法。
    
- 不转换也能跑，但容易混淆。
    

---

## QA 10：`build_vocab()` 如何构建词汇表？

### 问题

python

def build_vocab(
    self, corpus_iterable=None, corpus_file=None, update=False,
    progress_per=10000, keep_raw_vocab=False, trim_rule=None, **kwargs
):
    ...

如何构建词汇表的？

### 回答

#### 1. 词汇表的作用

词汇表是模型对所有唯一 token（如单词或图中节点）的统计和映射，包含：

- 每个 token 的唯一标识（如字符串 `"1"`）。
    
- token 的频率统计。
    
- 是否保留该 token（根据 `min_count` 过滤）。
    

#### 2. `build_vocab()` 的步骤

**（1）检查输入数据 `_check_corpus_sanity`**

- 验证 `corpus_iterable` 或 `corpus_file` 是否合法。
    
- 确保数据是可迭代的。
    

**（2）扫描词汇表 `scan_vocab`**

- 统计词频：遍历所有句子，计算每个 token 的出现次数。
    
    python
    
    # walks = [["1", "2", "3"], ["2", "4", "5"]]
    # 统计结果：{"1": 1, "2": 2, "3": 1, "4": 1, "5": 1}
    
- 每处理 `progress_per` 个 token 打印进度。
    

**（3）准备词汇表 `prepare_vocab`**

- 过滤低频词：根据 `min_count` 删除出现次数少于阈值的 token。
    
- 应用裁剪规则：可选 `trim_rule` 函数自定义过滤逻辑。
    
- 分配唯一索引：为保留的 token 分配整数 ID（如 `"2" → 0`）。
    

**（4）初始化权重矩阵 `prepare_weights`**

- 初始化两个矩阵：
    
    - 输入嵌入矩阵 `syn 0`：形状 `[vocab_size, vector_size]`。
        
    - 输出嵌入矩阵 `syn 1`：形状 `[vocab_size, vector_size]`（负采样时为 `syn 1 neg`）。
        

**（5）内存估算与事件记录**

- 计算内存占用 `estimate_memory`。
    
- 记录生命周期事件 `add_lifecycle_event`。
    

#### 3. 关键参数解析

|参数|作用|
|---|---|
|`corpus_iterable`|输入数据（如随机游走序列）|
|`min_count`|过滤低频词（默认 5），设为 0 则保留所有|
|`update`|是否增量更新词汇表|
|`trim_rule`|自定义词汇过滤规则|

#### 4. 示例代码

python

from gensim.models import Word 2 Vec
walks = [
    ["1", "2", "3"],
    ["2", "4", "5"],
    ["3", "1", "2"]
]
model = Word 2 Vec(vector_size=64, window=3, min_count=1, sg=1)
model.build_vocab(walks)
print("词汇表大小:", len(model.wv.key_to_index))
print("节点 '2' 的 ID:", model.wv.key_to_index["2"])
print("节点 '2' 的频率:", model.wv.get_vecattr("2", "count"))

#### 5. 输出结果

- **词汇表内容**：
    
    - `model.wv.key_to_index`：字典，token → 索引。
        
    - `model.wv.index_to_key`：列表，按索引顺序存储 token。
        
- **词频统计**：
    
    - `model.wv.get_vecattr(token, "count")`。
        

#### 6. 注意事项

- **输入数据格式**：token 必须是字符串。
    
- **增量更新 `update=True`**：新增 token 会合并到现有词汇表。
    
- **性能优化**：大规模数据建议用 `corpus_file`。
    

#### 7. 词汇表与后续训练的关系

- 训练时：模型通过 `key_to_index` 查找 token 的嵌入向量。
    
- 过滤影响：被 `min_count` 过滤的 token 无法获得嵌入向量。
    

### 关键结论

- `build_vocab()` 统计词频、过滤低频词、分配索引、初始化权重矩阵。
    
- 词汇表是 token → 索引的映射，是训练的基础。
    
- `min_count=0` 保留所有节点，适合图嵌入。
    

---

## QA 11：`train()` 方法是如何训练的？

### 问题

python

def train(
    self, corpus_iterable=None, corpus_file=None, total_examples=None,
    total_words=None, epochs=None, start_alpha=None, end_alpha=None,
    word_count=0, queue_factor=2, report_delay=1.0, compute_loss=False,
    callbacks=(), **kwargs
):
    ...

它是怎么训练的？

### 回答

#### 1. 训练前的准备

**（1）参数初始化**

- 设置学习率 `start_alpha` 和 `end_alpha`（线性衰减）。
    
- 检查 `epochs`、`total_examples` 等参数。
    

**（2）关键参数**

|参数|作用|
|---|---|
|`corpus_iterable`|输入数据|
|`total_examples`|总句子数|
|`epochs`|迭代次数|
|`compute_loss`|是否计算损失|

#### 2. 训练核心流程

**（1）逐轮训练**

python

for cur_epoch in range(self.epochs):
    for callback in callbacks:
        callback.on_epoch_begin(self)
    
    if corpus_iterable is not None:
        trained_word_count_epoch, raw_word_count_epoch, job_tally_epoch = self._train_epoch(...)
    else:
        trained_word_count_epoch, raw_word_count_epoch, job_tally_epoch = self._train_epoch_corpusfile(...)
    
    for callback in callbacks:
        callback.on_epoch_end(self)

**（2）滑动窗口生成样本**

对序列 `["1", "2", "3", "4"]`，`window=2`：

- 中心词 "2" → 上下文 `["1", "3", "4"]`。
    
- 中心词 "3" → 上下文 `["2", "4"]`。
    

**（3）参数更新**

- **Skip-gram（`sg=1`）**：对每个 `(center, context)` 对，更新中心词向量以预测上下文词。
    
    - 正向传播：计算上下文词概率。
        
    - 反向传播：通过负采样或层次 Softmax 优化损失。
        
- **CBOW（`sg=0`）**：用上下文词向量的平均预测中心词。
    

**（4）学习率衰减**

text

current_alpha = start_alpha - (start_alpha - end_alpha) * (word_count / total_words)

#### 3. 关键方法解析

**（1）`_train_epoch()`**

- 输入：当前轮次、语料库、回调函数。
    
- 流程：
    
    1. 将语料库分块，分配给工作线程。
        
    2. 每个线程处理数据块，生成 `(center, context)` 对。
        
    3. 调用 `train_batch()` 更新模型参数。
        

**（2）`train_batch()`**

- **负采样**（`negative > 0`）：
    
    - 对每个正样本 `(center, context)`，采样 `negative` 个负样本。
        
    - 更新向量使正样本得分提高，负样本得分降低。
        
- **层次 Softmax**（`hs=1`）：
    
    - 使用霍夫曼树加速概率计算，仅更新路径上的节点向量。
        

#### 4. 训练示例代码

python

from gensim.models import Word 2 Vec
walks = [["1", "2", "3"], ["2", "4", "5"], ["3", "1", "2"]]
model = Word 2 Vec(vector_size=64, window=3, min_count=1, sg=1, workers=4)
model.build_vocab(walks)
model.train(
    walks,
    total_examples=model.corpus_count,
    epochs=10,
    compute_loss=True
)
print("Latest loss:", model.get_latest_training_loss())

#### 5. 输出与模型更新

- **嵌入向量**：
    
    python
    
    print(model.wv["1"])
    
- **训练统计**：
    
    - `model.train_count`：训练次数。
        
    - `model.running_training_loss`：累计损失。
        

#### 6. 注意事项

- **数据格式**：token 必须是字符串。
    
- **学习率管理**：多次调用 `train()` 需手动管理 `start_alpha` 和 `end_alpha`。
    
- **性能调优**：
    
    - 增加 `workers` 加速训练。
        
    - 使用 `corpus_file` 减少内存占用。
        

#### 7. 训练过程总结

|阶段|输入|输出|
|---|---|---|
|滑动窗口|序列|(center, context) 对|
|优化|样本对|更新后的向量|
|最终|所有样本|嵌入矩阵|

### 关键结论

- `train()` 通过多轮迭代、滑动窗口、负采样/层次 Softmax 训练。
    
- 输入是语料库，输出是嵌入向量。
    
- 学习率线性衰减，`workers` 控制并行。
    

---

## QA 12：训练模型的输入是什么，输出是什么，是多少？

### 问题

整个训练模型的输入是什么，是多少？输出是什么，是多少？

### 回答

#### 1. 输入

**（1）训练数据格式**

- 类型：`List[List[str]]`。
    
- 示例：
    
    python
    
    walks = [
        ["1", "2", "4", "5", "4"],
        ["2", "4", "3", "1", "2"],
        ["3", "4", "5", "4", "3"],
        ...
    ]
    
- 每条序列长度：`walk_length`（如 5）。
    
- 序列数量：`num_nodes × walks_per_node`（如 10312 × 10 = 103120）。
    

**（2）关键参数**

|参数|作用|示例值|
|---|---|---|
|`vector_size`|嵌入维度|64, 128, 256|
|`window`|上下文窗口|3, 5|
|`sg`|训练算法|1 (Skip-gram)|
|`negative`|负采样数|5|
|`epochs`|迭代次数|10|

#### 2. 输出

**（1）嵌入矩阵**

- 形状：`(num_nodes, vector_size)`。
    
- `num_nodes`：词汇表中唯一节点数（≤ 10312）。
    
- `vector_size`：嵌入维度（如 64）。
    

**（2）访问方式**

python

model.wv["2"]  # 返回节点 "2" 的向量，形状 (64,)

**（3）示例输出**

python

[-0.053842, -0.005956, ..., 0.070181]  # 共 64 个浮点数

**（4）词汇表**

python

print(model.wv.key_to_index)  # {"1": 0, "2": 1, ...}
print(model.wv.get_vecattr("2", "count"))  # 节点 "2" 的出现次数

#### 3. 输入到输出的训练过程

**（1）数据流**

text

输入序列 → 滑动窗口 → 中心词-上下文对

例如：序列 `["1", "2", "4", "5", "4"]`，`window=2`：

- 中心词 "2" → 上下文 `["1", "4", "5"]`
    
- 中心词 "4" → 上下文 `["2", "5", "4"]`
    

**（2）参数更新（Skip-gram + 负采样）**

- 对每个 `(center, context)` 对：
    
    - 正向传播：计算 center 与 context 的相似度。
        
    - 反向传播：调整向量，使相似度提高。
        
    - 负采样：随机选择 `negative` 个非上下文词，降低相似度。
        

**（3）维度变化**

|步骤|数据形状|说明|
|---|---|---|
|输入序列|(num_sequences, walk_length)|103120 × 5|
|滑动窗口生成样本|(num_pairs,)|约 103120 × 5 × 3 对|
|输出嵌入矩阵|(num_nodes, vector_size)|10312 × 64|

#### 4. 示例代码

python

from gensim.models import Word 2 Vec
walks = generate_random_walks()  # 形状: (103120, 5)
model = Word 2 Vec(walks, vector_size=64, window=3, sg=1, negative=5, epochs=10)
print("节点 '2' 的嵌入:", model.wv["2"].shape)  # (64,)
print("词汇表大小:", len(model.wv.key_to_index))  # ≤10312

#### 5. 关键总结

|组件|类型/形状|说明|
|---|---|---|
|输入|List[List[str]]|随机游走序列|
|参数|window, vector_size 等|控制上下文和输出维度|
|输出|(num_nodes, vector_size)|每个节点的嵌入向量|
|训练|滑动窗口 + 负采样|优化中心词与上下文词相似度|

### 关键结论

- 输入：随机游走序列（List[List[str]]）。
    
- 输出：`(num_nodes, vector_size)` 的嵌入矩阵。
    
- 训练：滑动窗口 + Skip-gram + 负采样/层次 Softmax。
    

---

## QA 13：到底是整个 10 个节点输入，还是只输入序列？输出是 5 个概率还是 10 个概率？

### 问题

有 10 个节点，随机游走序列为 5，序列 `["1", "2", "4", "5", "3"]`，`window=2`：

中心词 "2" → 上下文 `["1", "4", "5"]`。

到底是将这个随机游走序列作为输入（输入 "2" 为 1 其余为 0，标签 "1","4","5" 为 1 其余为 0，输出 5 个概率）；还是整个 10 个节点输入（输入 "2" 为 1 其余为 0，标签 "1","4","5" 为 1 其余为 0，输出 10 个概率）？

我的输出不应该是概率然后通过交叉熵反向传播吗？为什么直接输出了词嵌入向量？

### 回答

#### 1. 输入输出的真实形式

**（1）输入不是 One-hot，而是直接查找嵌入向量**

- **误解**：输入是 One-hot 向量（如 `[0,1,0,...,0]`），然后通过全连接层输出概率。
    
- **实际**：Word 2 Vec 跳过了显式的 One-hot 编码，直接通过**嵌入查找表**获取中心词的向量：
    
    - 输入是**节点 ID**（如 `"2"`），直接查表得到对应的 d 维向量 `v_2`（如 64 维）。
        
    - 不涉及 One-hot 到隐层的矩阵乘法。
        

**（2）输出是多个二分类任务（不是多分类 Softmax）**

- **误解**：模型输出一个 10 维的概率分布（对所有节点做 Softmax）。
    
- **实际**：Skip-gram 对**每个上下文词**单独做二分类（Sigmoid）：
    
    - 正样本：`("2", "1")`、`("2", "4")`、`("2", "5")`（目标概率→1）。
        
    - 负样本：随机采样 `negative` 个非上下文词（如 `"3"`, `"7"`），目标概率→0。
        

#### 2. 具体训练步骤（你的例子）

序列 `["1", "2", "4", "5", "3"]`，`window=2`：

**滑动窗口生成样本：**

- 中心词 "2"（位置=1）→ 上下文 `["1", "4", "5"]`。
    

**训练样本处理：**

- **输入**：中心词 "2"。
    
    - 直接查找 "2" 的嵌入向量 `v_2`（形状 `(64,)`）。
        
- **输出**：
    
    - 对每个上下文词 `c ∈ ["1", "4", "5"]`：
        
        - 计算点积：`score = v_2 · u_c`。
            
        - 通过 Sigmoid 得到概率：`P(c|"2") = σ(score)`。
            
    - 对负样本词 `n ∈ ["3", "7"]`：
        
        - 计算 `P(n|"2") = σ(v_2 · u_n)`，希望接近 0。
            

**损失函数**：二元交叉熵（Binary Cross-Entropy）。

- 正样本：`loss = -log(P(c|"2"))`。
    
- 负样本：`loss = -log(1 - P(n|"2"))`。
    

**参数更新**：通过反向传播更新：

- 中心词向量 `v_2`。
    
- 上下文词向量 `u_1, u_4, u_5`。
    
- 负样本词向量 `u_3, u_7`。
    

#### 3. 为什么输出是词嵌入向量？

- **最终目标**：训练完成后，你需要的是**节点的嵌入向量**（即 `model.wv["2"]`），而非概率分布。
    
- **概率是中间过程**：训练时通过概率优化调整向量，但最终保存的是优化后的嵌入矩阵。
    
- **输入层矩阵 `W_in`**：行是节点的嵌入向量（`W_in[2]` 是 "2" 的向量）。
    
- **输出层矩阵 `W_out`**：行是节点的上下文向量（训练后通常丢弃）。
    

#### 4. 与经典神经网络的对比

|步骤|传统神经网络（Softmax）|Word 2 Vec（Skip-gram + 负采样）|
|---|---|---|
|输入|One-hot 向量|直接查表获取嵌入向量|
|输出层|全连接 + Softmax（多分类）|点积 + Sigmoid（多个二分类）|
|参数量|输入层 V×d，输出层 d×V|输入层 V×d，输出层 d×V（稀疏更新）|
|计算效率|低（需计算全词表 Softmax）|高（仅更新正负样本）|

#### 5. 你的例子：10 个节点

假设节点 ID 为 `["0", "1", ..., "9"]`：

- 输入序列 `["1", "2", "4", "5", "3"]`：
    
    - 中心词 "2" 的上下文是 `["1", "4", "5"]`。
        
    - **不是**用 One-hot 输入 `[0,1,0,...,0]`，而是直接取 `v_2`。
        
    - **不是**输出 10 维概率，而是对 "1", "4", "5" 分别计算 `P(c|"2")`，并采样负样本（如 "0", "7"）计算 `P(n|"2")`。
        

#### 6. 总结

|组件|实际情况|
|---|---|
|输入|节点 ID（如 "2"）→ 直接查表得到 d 维向量|
|输出|对每个 (center, context) 对做二分类（Sigmoid）|
|监督信号|正/负样本标记（负采样）或霍夫曼树路径编码（HS）|
|最终产物|训练完成后，`model.wv` 存储所有节点的 d 维嵌入向量|
|输出维度|**不是** 5 或 10 维概率，而是每个上下文词一个二分类概率|

### 关键结论

- 输入不是 One-hot，而是直接查表得到的嵌入向量。
    
- 输出不是 5 或 10 维概率，而是多个独立的二分类任务。
    
- 最终保留的是嵌入向量，概率只是训练时的中间产物。
    
- 这种设计避免了全词表 Softmax，大幅提升效率。
    

---

## QA 14：结合 `w 2 v_fast_sentence_sg_hs` 说明输入、输出、标签、损失

### 问题

c

cdef void w 2 v_fast_sentence_sg_hs(
    const np.uint 32_t *word_point, const np.uint 8_t *word_code, const int codelen,
    REAL_t *syn 0, REAL_t *syn 1, const int size,
    const np.uint 32_t word 2_index, const REAL_t alpha, REAL_t *work, REAL_t *words_lockf,
    const np.uint 32_t lockf_len, const int _compute_loss, REAL_t *_running_training_loss_param
) nogil:

这个模型的输入是什么？为多少？输出是什么？为多少？标签（监督信号）是什么？要优化什么损失函数？

### 回答

#### 1. 输入

**（1）输入数据**

- **中心词**：`word 2_index`，词汇表中的整数索引（如 2 表示节点 "2"）。
    
    - 实际输入：直接查找 `syn 0[word 2_index]` 获取中心词的 d 维向量。
        
    - 形状：`(d,)`（如 64 维）。
        
- **上下文词的层次编码**：
    
    - `word_point`：霍夫曼树路径的节点索引列表（如 `[5, 3, 1]`）。
        
    - `word_code`：霍夫曼树路径的二进制编码（如 `[1, 0, 1]`，1=右，0=左）。
        
    - `codelen`：路径长度（如 3）。
        

**（2）模型参数**

|参数|含义|形状|
|---|---|---|
|`syn 0`|输入层嵌入矩阵|`(vocab_size, d)`|
|`syn 1`|霍夫曼树非叶子节点参数|`(num_inner_nodes, d)`|
|`alpha`|当前学习率|标量|

#### 2. 输出

- **无显式输出**：该函数通过修改 `syn 0` 和 `syn 1` 的权重**就地更新**模型参数。
    
- **隐式输出**：优化后的中心词向量和霍夫曼树节点向量。
    

#### 3. 标签（监督信号）

- **目标**：让中心词 `word 2_index` 的向量能够正确预测上下文词的霍夫曼树路径。
    
- **监督信号**：
    
    - `word_code` 的二进制编码定义了正确的路径方向。
        
    - 对路径上的每个非叶子节点：
        
        - 若 `word_code[b] = 1`，希望向右子树走的概率高。
            
        - 若 `word_code[b] = 0`，希望向左子树走的概率高。
            

#### 4. 损失函数

- **类型**：负对数似然（Negative Log-Likelihood）。
    
- **计算方式**：
    
    - 对霍夫曼树路径上的每个节点 b：
        
        - 计算点积：`f_dot = syn 0[word 2_index] · syn 1[word_point[b]]`。
            
        - 通过 Sigmoid 转换：`prob = σ(f_dot)`。
            
        - 计算当前节点的损失：
            
            - 若 `word_code[b] = 1`：`loss_b = -log(prob)`。
                
            - 若 `word_code[b] = 0`：`loss_b = -log(1 - prob)`。
                
    - 总损失：所有路径节点损失的和（代码中 `_running_training_loss_param[0] -= lprob`）。
        

#### 5. 关键代码步骤解析

**（1）前向传播**

c

f_dot = our_dot(&size, &syn 0[row 1], &ONE, &syn 1[row 2], &ONE);
f = EXP_TABLE[\<int>((f_dot + MAX_EXP) * (EXP_TABLE_SIZE / MAX_EXP / 2))];

- `f_dot`：中心词向量与霍夫曼树节点向量的点积。
    
- `f`：通过预计算的 `EXP_TABLE` 近似 Sigmoid 输出。
    

**（2）损失计算**

c

sgn = (-1)**word_code[b];
lprob = sgn * f_dot;
lprob = LOG_TABLE[...];
_running_training_loss_param[0] -= lprob;

**（3）反向传播与参数更新**

c

g = (1 - word_code[b] - f) * alpha;
our_saxpy(&size, &g, &syn 1[row 2], &ONE, work, &ONE);
our_saxpy(&size, &g, &syn 0[row 1], &ONE, &syn 1[row 2], &ONE);

- `g`：梯度信号，根据预测误差和学习率计算。
    

#### 6. 输入输出形状总结

|组件|形状/类型|说明|
|---|---|---|
|输入中心词|标量 `word 2_index`|词汇表中的整数索引|
|输入上下文编码|`word_point`, `word_code`|霍夫曼树路径|
|模型参数|`syn 0: (vocab_size, d)`|中心词嵌入矩阵|
||`syn 1: (num_inner_nodes, d)`|霍夫曼树参数|
|损失输出|标量|累计负对数似然|

#### 7. 与负采样的区别

|方法|机制|适用场景|
|---|---|---|
|层次 Softmax|霍夫曼树路径，仅更新路径节点|大词汇表|
|负采样|采样负样本，更新正负样本|分布式训练，小词汇表|

#### 8. 示例流程

假设：

- 中心词 "2" 的索引 `word 2_index = 2`。
    
- 上下文词 "1" 的霍夫曼树路径 `word_point = [5, 3]`，`word_code = [1, 0]`。
    

训练步骤：

1. 取 `syn 0[2]` 和 `syn 1[5]`，计算 `f_dot = syn 0[2] · syn 1[5]`。
    
2. 根据 `word_code[0]=1`，希望 `σ(f_dot)` 接近 1，计算梯度并更新。
    
3. 对下一个节点 `syn 1[3]`，根据 `word_code[1]=0`，希望 `σ(f_dot)` 接近 0，更新参数。
    

### 关键结论

- 输入：中心词索引 + 上下文词霍夫曼树路径。
    
- 输出：无显式输出，就地更新 `syn 0` 和 `syn 1`。
    
- 标签：霍夫曼树路径的二进制编码。
    
- 损失：路径上每个节点的二分类交叉熵。
    
- 最终产物：`syn 0` 是节点嵌入矩阵。
    

---

## QA 15：霍夫曼树的叶子节点大小是不是就是滑动窗口的大小？

### 问题

霍夫曼树的叶子节点的大小是不是就是滑动窗口的大小？

### 回答

#### 1. 霍夫曼树的叶子节点数量

- **叶子节点数 = 词汇表大小（vocab_size）**。
    
- 你的例子中图有 10 个节点，则霍夫曼树有 10 个叶子节点。
    
- **与滑动窗口无关**：无论 `window` 是 2、5 还是其他值，叶子节点数始终等于词汇表大小。
    

#### 2. 滑动窗口的作用

- `window` 决定**每个中心词的上下文词数量**。
    
- 例如 `window=2` 时，中心词 "2" 的上下文是 `["1", "4", "5"]`。
    
- 影响训练样本数量，但**不改变霍夫曼树结构**。
    

#### 3. 层次 Softmax 的训练过程

**（1）对每个 (center, context) 对：**

- **中心词**：如 "2"，通过 `syn 0[2]` 获取向量。
    
- **上下文词**：如 "1"，通过其霍夫曼树路径更新参数：
    
    - 假设 "1" 的路径是 `word_point=[5,3]`, `word_code=[1,0]`：
        
        - 对非叶子节点 5：计算 `P(right|"2") = σ(syn 0[2]·syn 1[5])`，根据 `word_code[0]=1` 调整参数。
            
        - 对非叶子节点 3：计算 `P(right|"2") = σ(syn 0[2]·syn 1[3])`，根据 `word_code[1]=0` 调整参数。
            

**（2）关键点：**

- **叶子节点**：仅用于表示词汇表中的节点，不参与参数更新。
    
- **非叶子节点**：通过 `syn 1` 矩阵存储参数，用于计算路径概率。
    

#### 4. 输入输出与霍夫曼树的关系

|组件|来源|是否依赖滑动窗口|
|---|---|---|
|霍夫曼树叶子数|词汇表大小（10 个节点）|❌ 无关|
|上下文词数量|滑动窗口（`window=2`）|✅ 直接相关|
|训练样本量|walks 数量 × 窗口内上下文数|✅ 间接相关|

#### 5. 你的例子：10 节点，window=2

- **霍夫曼树**：10 个叶子（对应 "0" 到 "9"），非叶子节点数 = 9。
    
- **滑动窗口**：
    
    - 序列 `["1", "2", "4", "5", "3"]` 中，中心词 "2" 的上下文是 `["1", "4", "5"]`。
        
    - 对每个上下文词（如 "1"），使用其霍夫曼树路径更新参数，路径长度与窗口无关。
        

#### 6. 总结

|概念|决定因素|
|---|---|
|叶子节点数|词汇表大小（图的唯一节点数）|
|滑动窗口大小|每个中心词的上下文词数量|
|霍夫曼树结构|与窗口无关，仅由词汇表和词频决定|

### 关键结论

- 霍夫曼树叶子数 = 词汇表大小，**不是**滑动窗口大小。
    
- 滑动窗口只影响上下文词数量，不影响霍夫曼树结构。
    
- 层次 Softmax 复杂度 O(log V)，与窗口无关。
    

---

## QA 16：相似的关系（最终得到的嵌入向量）也是一种标签，所以 DeepWalk 也是一种分类任务，对吗？

### 问题

相似的关系（最终得到的嵌入向量）也是一种标签，所以 DeepWalk 也是一种分类任务，你说的对吗？

### 回答

#### 1. DeepWalk 的无监督本质

**（1）训练目标：共现关系建模**

- **输入**：随机游走序列（如 `["1", "2", "4"]`）。
    
- **目标**：让中心节点（如 "2"）的嵌入向量能够预测其上下文节点（如 "1" 和 "4"）。
    
- **方法**：通过 Skip-gram 的负采样或层次 Softmax 优化，**没有显式的类别标签**。
    

**（2）与分类任务的区别**

|特性|DeepWalk/Skip-gram|分类任务（如节点分类）|
|---|---|---|
|监督信号|上下文节点的共现性（无标签）|明确的类别标签（如 "A 类"）|
|输出|节点嵌入向量|类别概率分布|
|损失函数|负对数似然（无监督）|交叉熵（监督）|

#### 2. 为什么说嵌入向量"隐含"相似性？

- **嵌入向量的几何意义**：在向量空间中，频繁共现的节点会被映射到相近的位置。
    
- **相似性作为伪标签**：这种相似性可以看作一种**隐式的关系标签**（例如："节点 A 和 B 相似"），但它是数据驱动的结果，而非人工标注的类别。
    

#### 3. DeepWalk 如何用于分类？

虽然 DeepWalk 本身是无监督的，但其生成的嵌入向量可以作为**特征**输入到监督分类模型中：

**（1）无监督预训练**

- 用 DeepWalk 生成所有节点的嵌入。
    

**（2）监督微调**

- 将嵌入向量作为特征，训练分类器（如逻辑回归）预测节点类别。
    

**示例代码：**

python

from sklearn.linear_model import LogisticRegression
model = Word 2 Vec(walks, vector_size=64, window=3, sg=1, hs=1)
X = [model.wv[node] for node in nodes]
y = labels
clf = LogisticRegression().fit(X, y)

#### 4. 你的观点：相似性作为"标签"

- **严格意义的"标签"**：指监督学习中的真实类别（如 `y=[0, 1, 0]`），而 DeepWalk **没有**此类标签。
    
- **广义的"关系标签"**：嵌入向量隐含的相似性可以视为一种**伪标签**（通过数据分布自动生成），用于衡量节点间的关联强度。
    

#### 5. 总结

- **DeepWalk 是无监督的**：它学习的是节点共现模式，而非直接分类。
    
- **嵌入向量编码相似性**：这种相似性可用于下游分类任务，但本身不是分类。
    
- **类比**：
    
    - 像 Word 2 Vec 学习"词义相似性"而非词性标注。
        
    - 像 PCA 降维后数据可用于分类，但 PCA 本身是无监督的。
        

### 关键结论

- DeepWalk **不是**分类任务，它是无监督（自监督）嵌入学习。
    
- 嵌入向量隐含相似性，可作为下游分类任务的特征。
    
- 相似性可视为"伪标签"，但不是人工标注的类别标签。
    

---

## QA 17：无监督就是没有标签吗？

### 问题

无监督就是没有标签吗？

### 回答

#### 1. 无监督学习的核心定义

- **传统理解**：无监督学习确实不依赖人工标注的标签（如类别标签 y），而是从数据本身发现模式。
    
- **更广义的理解**：
    
    - 数据自带的**隐式关系**（如节点共现、序列顺序）可视为一种"天然标签"。
        
    - 目标函数中的**隐式监督信号**：例如 Skip-gram 的上下文预测任务中，共现节点对就是"伪标签"。
        

#### 2. DeepWalk 的"伪监督"特性

虽然 DeepWalk 被归类为无监督学习，但其训练过程隐含了一种**自监督（Self-Supervised）**机制：

**（1）输入与伪标签**

|组件|来源|是否人工标注|
|---|---|---|
|输入数据|随机游走序列|❌ 无|
|伪标签（目标）|中心词与上下文词的共现对|❌ 无（来自数据分布）|

**（2）损失函数的监督信号**

- Skip-gram 的目标：最大化 `P(context|center)`。
    
- 上下文词的选择本质上是**数据自动生成的监督信号**。
    
- 例如：序列 `["1","2","4"]` 中，"2" 的上下文 `["1","4"]` 就是模型需要拟合的"目标"。
    

#### 3. 与纯无监督学习的对比

|方法|监督信号来源|是否需人工标签|典型算法|
|---|---|---|---|
|纯无监督|数据分布（如聚类、降维）|❌ 无需|K-Means, PCA|
|自监督（如 DeepWalk）|数据内部生成的关系（如共现）|❌ 无需|Skip-gram, Node 2 Vec|
|监督学习|人工标注的标签（如类别）|✅ 需要|逻辑回归, CNN|

#### 4. 为什么说 DeepWalk 是"无监督"的？

- **无需人工干预**：所有"标签"来自数据本身的连接关系。
    
- **无明确分类目标**：模型不知道节点类别，只学习共现模式。
    
- **下游任务无关性**：生成的嵌入可用于分类、聚类、链接预测等多种任务。
    

#### 5. 关键结论

- **无监督 ≠ 完全没有标签**：而是指不依赖人工标注，但可能利用数据内在的关联性作为隐式监督信号。
    
- **DeepWalk 的"伪标签"**：来自随机游走序列的上下文共现关系，是数据自动生成的。
    
- **自监督学习的桥梁**：这类方法介于传统无监督和监督学习之间，属于无监督学习的扩展范畴。
    

#### 6. 类比说明

- **纯无监督**：像将一堆未分类的图片根据颜色自动分组（无任何指导）。
    
- **自监督（DeepWalk）**：像通过"拼图游戏"学习图片内容（利用图片自身的局部关系作为指导）。
    
- **监督学习**：像根据已标注的图片学习分类（明确知道每张图是"猫"或"狗"）。
    

### 关键结论

- 无监督 ≠ 没有标签，而是不依赖人工标注。
    
- DeepWalk 属于自监督，利用数据自身的共现关系作为隐式监督信号。
    
- 自监督介于无监督和监督之间，是无监督的扩展。
    

---

## QA 18：向量表征是无监督还是自监督？

### 问题

向量表征是无监督还是自监督？

### 回答

#### 1. 向量表征的三种类型

|类型|监督信号|代表方法|
|---|---|---|
|无监督|无任何标签，仅利用数据分布|PCA, K-Means, 自编码器|
|自监督|从数据自身构造伪标签|Word 2 Vec, DeepWalk, BERT, SimCLR|
|监督|人工标注标签|逻辑回归, CNN 分类|

#### 2. 图嵌入中的向量表征

**（1）DeepWalk / Node 2 Vec**

- **自监督**：通过随机游走生成序列，用 Skip-gram 的共现关系作为伪标签。
    
- 不需要人工标签。
    

**（2）GCN / GAT（图神经网络）**

- **监督**：使用节点分类标签训练。
    
- **半监督**：只用部分节点标签。
    
- **无监督**：使用重构损失或对比损失。
    

**（3）GraphSAGE**

- 可以监督、半监督或无监督。
    

**（4）矩阵分解（如 SVD）**

- **无监督**：直接分解邻接矩阵。
    

#### 3. 为什么 Word 2 Vec/DeepWalk 归为自监督？

- **伪标签来自数据自身**：上下文词是数据自动生成的"目标"。
    
- **无需人工标注**：完全从数据中学习。
    
- **目标函数是预测任务**：预测上下文（Skip-gram）或中心词（CBOW）。
    

#### 4. 自监督 vs 无监督的区别

|维度|无监督|自监督|
|---|---|---|
|监督信号|无|数据自身构造的伪标签|
|目标|发现数据分布|预测数据的一部分|
|代表|PCA, K-Means|Word 2 Vec, BERT, DeepWalk|
|是否需要预测任务|否|是|

#### 5. 总结

- **DeepWalk / Node 2 Vec / Word 2 Vec**：自监督。
    
- **PCA / K-Means / SVD**：无监督。
    
- **GCN 分类**：监督或半监督。
    
- **对比学习（SimCLR, GraphCL）**：自监督。
    

### 关键结论

- 向量表征可以是无监督、自监督或监督的。
    
- DeepWalk / Word 2 Vec 属于**自监督**。
    
- 自监督是无监督的扩展，利用数据自身构造伪标签。
    
- 区分关键：是否有预测任务 + 伪标签是否来自数据自身。
    

---

## 全局总结

### 一、数据流

text

blogcatalog.mat
├── network (10312×10312) → 建图 G
└── group/graph (10312×39) → 分类标签
G → 随机游走 → walks (List[List[str]])
walks → Word 2 Vec → 嵌入矩阵 (10312 × d)
嵌入矩阵 + labels → 分类器 → 节点分类

### 二、关键参数

|参数|作用|阶段|
|---|---|---|
|`walk_length`|序列长度|随机游走|
|`gamma`|每节点游走次数|随机游走|
|`window`|上下文窗口|Word 2 Vec 训练|
|`vector_size`|嵌入维度|Word 2 Vec 训练|
|`sg`|Skip-gram / CBOW|Word 2 Vec 训练|
|`hs` / `negative`|层次 Softmax / 负采样|Word 2 Vec 训练|
|`epochs`|迭代次数|Word 2 Vec 训练|
|`min_count`|低频过滤|词汇表构建|

### 三、核心概念

|概念|说明|
|---|---|
|随机游走|把图变成序列|
|Skip-gram|用中心词预测上下文|
|层次 Softmax|霍夫曼树加速，叶子数 = 词汇表大小|
|负采样|采样负样本，二分类|
|嵌入向量|训练后 `syn 0`，即节点向量|
|自监督|用数据自身构造伪标签|
|无监督|不依赖人工标签|

### 四、常见误区

|误区|正确|
|---|---|
|输入是 One-hot|直接查表得嵌入向量|
|输出是 V 维概率|多个二分类|
|标签是 Multi-hot|霍夫曼路径编码或正负标记|
|walk_length 和 window 冲突|独立，控制不同阶段|
|霍夫曼叶子数 = 窗口大小|叶子数 = 词汇表大小|
|DeepWalk 是分类|自监督嵌入，嵌入可用于分类|
|无监督 = 无标签|无人工标签，可有隐式监督|

### 五、最终产物

- **`syn 0`**：节点嵌入矩阵 `(num_nodes, vector_size)`。
    
- **`model.wv["node_id"]`**：访问某个节点的向量。
    
- **`model.wv.most_similar("node_id")`**：找相似节点。
    
- **保存格式**：
    
    text
    
    10312 128
    4838 -0.053842 -0.005956 ... (128 个浮点数)
    ...
    

### 六、下游任务

- **节点分类**：嵌入 + 标签 → 分类器。
    
- **链接预测**：嵌入相似度 → 预测边。
    
- **聚类**：嵌入 → K-Means。
    
- **可视化**：嵌入 → t-SNE / UMAP。