生物信息学入门和图神经网络
该流程图展示了一个严谨的 lincRNA 鉴定流程:它首先从多种RNA-seq数据中组装转录本,然后将其定位到基因间区,最后通过一系列严格的过滤条件(长度、编码潜能、与已知RNA或蛋白结构域的重叠)来筛选出最终的 lincRNA 候选集。想象你是一个学生,准备一场满分100分的大型考试。$D_{all}$ (所有数据):代表“题库”,包含了这场考试可能出现的所有题目(比如10,000道题)。$D_
这是一个生物信息学分析流程图,详细说明了如何从RNA测序数据中鉴定出候选的“长链基因间非编码RNA”(lincRNA)。
以下是流程图的详细步骤解读:
-
起始数据 (Input Data):
-
scRNA-seq/snRNA-seq and bulk RNA-seq data -
解读:流程的起点是三种不同类型的RNA测序数据:单细胞RNA测序 (scRNA-seq)、单核RNA测序 (snRNA-seq) 和常规的批量RNA测序 (bulk RNA-seq)。使用多种数据源可以更全面地捕获转录本。
-
-
数据预处理 (Data Pre-processing):
-
FastQC, TrimGalore -
解读:这是标准的质控和清洗步骤。
FastQC用于检查原始测序数据的质量,TrimGalore用于去除低质量的碱基和测序接头 (adapter)。
-
-
比对与转录本组装 (Alignment & Assembly):
-
HISAT2, StringTie -
解读:
HISAT2是一个将清洗后的测序读长(reads)比对到参考基因组上的工具。StringTie则根据比对结果,将这些读长组装成完整的转录本(transcripts)。
-
-
合并转录本 (Transcript Collection):
-
Known lncRNA transcripts(已知的lncRNA转录本) -
Assembled transcripts(新组装的转录本) -
解读:这一步将上一步中新组装(可能包含未知的)转录本与数据库中已知的lncRNA转录本合并在一起,形成一个更完整的转录本集合。
-
-
定位 (Localization):
-
Locating in the intergenic regions(定位于基因间区) -
解读:这是定义 "intergenic" (基因间) 的关键步骤。流程会筛选出那些位于已知蛋白质编码基因 之间 的转录本。
-
-
过滤标准 (Filtering criteria):
-
这是最核心的步骤,用于筛选出真正的 lincRNA。一个转录本必须同时满足以下所有五个条件:
-
-
Length > 200 bp:长度必须大于200个碱基对。这是定义 "长链" (long) non-coding RNA (lncRNA) 的标准阈值。 -
ORF < 100 aa:ORF (Open Reading Frame, 开放阅读框) 必须小于100个氨基酸。这是为了排除那些可能编码较短肽链的转录本。 -
No coding potential:没有编码潜力。这一步会使用专门的生物信息学工具(如 CPC, CPAT, Pfam等)来预测该RNA序列是否具有被翻译成功能性蛋白质的潜力。 -
No overlap with housekeeping ncRNAs:不与 "管家"非编码RNA(如 rRNA, tRNA, snRNA, snoRNA 等)重叠。这是为了排除那些已知的、具有特定结构或功能的非编码RNA。 -
No known protein domains:不包含任何已知的蛋白质结构域。这是另一重保险,确保该转录本不编码蛋白质。 -
最终输出 (Output):
-
lincRNA candidates(lincRNA 候选者) -
解读:通过上述所有步骤筛选后,剩下的转录本被认为是高可信度的 lincRNA 候选者,可以用于后续的深入研究。
-
总结
该流程图展示了一个严谨的 lincRNA 鉴定流程:它首先从多种RNA-seq数据中组装转录本,然后将其定位到基因间区,最后通过一系列严格的过滤条件(长度、编码潜能、与已知RNA或蛋白结构域的重叠)来筛选出最终的 lincRNA 候选集。
宝可梦和数码宝贝分类问题——机器学习
合并为一个poke代码
python
-- coding: utf-8 --
import hashlib import shutil from pathlib import Path
= 配置:按需修改 =
src1 = Path(r"C:\Users\32110\Desktop\ポケモン\poke_vs_digi\data\poke_new") src2 = Path(r"C:\Users\32110\Desktop\ポケモン\poke_vs_digi\data\poke_old") dst = Path(r"C:\Users\32110\Desktop\ポケモン\poke_vs_digi\data\poke_all")
IMG_EXTS = {".jpg", ".jpeg", ".png", ".bmp", ".gif", ".webp", ".tif", ".tiff"} dry_run = False # 先想看计划不实际复制 -> True;确认没问题再改回 False
def iter_images(folder: Path): for p in folder.rglob("*"): if p.is_file() and p.suffix.lower() in IMG_EXTS: yield p
def file_sha1(p: Path, block=1024 * 1024): h = hashlib.sha1() with p.open("rb") as f: while True: b = f.read(block) if not b: break h.update(b) return h.hexdigest()
def main(): assert src1.exists(), f"不存在:{src1}" assert src2.exists(), f"不存在:{src2}" dst.mkdir(parents=True, exist_ok=True)
# 记录目标中已有文件的hash,避免重复拷贝
print("扫描目标文件夹以建立去重索引……")
seen = {} # sha1 -> filename
for q in dst.rglob("*"):
if q.is_file() and q.suffix.lower() in IMG_EXTS:
try:
seen[file_sha1(q)] = q.name
except Exception as e:
print(f"[跳过损坏文件] {q}: {e}")
total, copied, skipped_dup = 0, 0, 0
for src in (src1, src2):
print(f"\n处理:{src}")
for p in iter_images(src):
total += 1
try:
h = file_sha1(p)
except Exception as e:
print(f"[读文件失败] {p}: {e}")
continue
if h in seen:
skipped_dup += 1
# 可换成 verbose 打印:print(f"[重复] {p} == {seen[h]}")
continue
# 生成不重名的文件名
name = p.name
stem, suf = p.stem, p.suffix
out = dst / name
k = 1
while out.exists():
out = dst / f"{stem}_{k}{suf}"
k += 1
print(("[计划复制] " if dry_run else "[复制] ") + f"{p} -> {out}")
if not dry_run:
shutil.copy2(p, out)
seen[h] = out.name
copied += 1
print("\n=== 总结 ===")
print(f"发现图片:{total}")
print(f"复制新文件:{copied}")
print(f"跳过重复:{skipped_dup}")
print(f"目标目录:{dst}")
if name == "main": main()
```python
# -*- coding: utf-8 -*-
#直接运行代码分类
"""
run_poke_digi.py
- 从 data/poke 与 data/digi 读取图片
- 划分 train/val/test
- 微调 ResNet18 并保存 best 模型
- 输出测试集报告 & 全量逐图预测至 outputs/classification_result.csv
"""
import os, json, random, csv, pandas as pd, numpy as np, torch
from pathlib import Path
from torch import nn
from torch.utils.data import DataLoader, random_split
from torchvision import datasets, transforms, models
from sklearn.metrics import classification_report, confusion_matrix
# ========= 配置 =========
DATA_DIR = r"C:\Users\32110\Desktop\ポケモン\poke_vs_digi\data"
SAVE_DIR = os.path.join(os.path.dirname(__file__) or ".", "outputs")
os.makedirs(SAVE_DIR, exist_ok=True)
IMG_SIZE = 224
BATCH = 32
EPOCHS = 6
LR = 1e-3
SEED = 42
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
USE_PRETRAINED = True # 如果无网络下载权重失败,设为 False
random.seed(SEED); np.random.seed(SEED); torch.manual_seed(SEED)
# ========= 变换 =========
tfm_train = transforms.Compose([
transforms.Resize((IMG_SIZE, IMG_SIZE)),
transforms.RandomHorizontalFlip(),
transforms.ColorJitter(0.2,0.2,0.2,0.1),
transforms.ToTensor(),
transforms.Normalize([0.485,0.456,0.406],[0.229,0.224,0.225]),
])
tfm_eval = transforms.Compose([
transforms.Resize((IMG_SIZE, IMG_SIZE)),
transforms.ToTensor(),
transforms.Normalize([0.485,0.456,0.406],[0.229,0.224,0.225]),
])
# ========= 数据 =========
full = datasets.ImageFolder(DATA_DIR, transform=tfm_train)
assert len(full.classes)==2 and set(full.classes)==set(["poke","digi"]) or True
num_train = int(0.8 * len(full))
num_val = int(0.1 * len(full))
num_test = len(full) - num_train - num_val
train_set, val_set, test_set = random_split(
full, [num_train, num_val, num_test],
generator=torch.Generator().manual_seed(SEED)
)
val_set.dataset.transform = tfm_eval
test_set.dataset.transform = tfm_eval
train_loader = DataLoader(train_set, batch_size=BATCH, shuffle=True, num_workers=0)
val_loader = DataLoader(val_set, batch_size=BATCH, shuffle=False, num_workers=0)
test_loader = DataLoader(test_set, batch_size=BATCH, shuffle=False, num_workers=0)
idx_to_cls = {v:k for k,v in full.class_to_idx.items()}
with open(os.path.join(SAVE_DIR, "label_map.json"), "w", encoding="utf-8") as f:
json.dump(idx_to_cls, f, ensure_ascii=False, indent=2)
# ========= 模型 =========
if USE_PRETRAINED:
weights = models.ResNet18_Weights.DEFAULT
else:
weights = None
model = models.resnet18(weights=weights)
model.fc = nn.Linear(model.fc.in_features, 2)
model.to(DEVICE)
opt = torch.optim.Adam(model.parameters(), lr=LR)
crit = nn.CrossEntropyLoss()
def run_epoch(loader, train=True):
model.train() if train else model.eval()
tot_loss, tot_correct, tot = 0.0, 0, 0
with torch.set_grad_enabled(train):
for x, y in loader:
x, y = x.to(DEVICE), y.to(DEVICE)
out = model(x)
loss = crit(out, y)
if train:
opt.zero_grad(); loss.backward(); opt.step()
tot_loss += loss.item() * x.size(0)
tot_correct += (out.argmax(1) == y).sum().item()
tot += x.size(0)
return tot_loss/tot, tot_correct/tot
best_val, best_path = 0.0, os.path.join(SAVE_DIR, "best_resnet18.pth")
for ep in range(1, EPOCHS+1):
tr_loss, tr_acc = run_epoch(train_loader, True)
va_loss, va_acc = run_epoch(val_loader, False)
print(f"Epoch {ep}/{EPOCHS} | train acc {tr_acc:.3f} loss {tr_loss:.3f} | val acc {va_acc:.3f} loss {va_loss:.3f}")
if va_acc > best_val:
best_val = va_acc
torch.save(model.state_dict(), best_path)
# ========= 测试报告 =========
model.load_state_dict(torch.load(best_path, map_location=DEVICE))
model.eval()
y_true, y_pred = [], []
with torch.no_grad():
for x, y in test_loader:
x = x.to(DEVICE)
logits = model(x)
y_true.extend(y.numpy().tolist())
y_pred.extend(logits.argmax(1).cpu().numpy().tolist())
print("Label map:", idx_to_cls)
print("Confusion matrix:\n", confusion_matrix(y_true, y_pred))
print(classification_report(y_true, y_pred, target_names=[idx_to_cls[0], idx_to_cls[1]]))
print(f"Best model saved to: {best_path}")
# ========= 导出全量逐图预测到 CSV =========
# 对 data 目录下所有图片做一次前向并写到 outputs/classification_result.csv
from PIL import Image
exts = {".jpg",".jpeg",".png",".bmp",".webp",".tif",".tiff"}
from PIL import Image
def predict_one(path: Path):
img = Image.open(path).convert("RGB")
x = tfm_eval(img).unsqueeze(0).to(DEVICE)
with torch.no_grad():
p = torch.softmax(model(x), dim=1)[0]
pred_idx = int(p.argmax().item())
# 关键修改:用整数键访问
label = idx_to_cls[pred_idx] if pred_idx in idx_to_cls else idx_to_cls[str(pred_idx)]
return label, float(p[pred_idx].item())
rows = []
for fp in Path(DATA_DIR).rglob("*"):
if fp.is_file() and fp.suffix.lower() in exts:
label, conf = predict_one(fp)
rows.append({"path": str(fp), "pred": label, "confidence": conf})
csv_path = os.path.join(SAVE_DIR, "classification_result.csv")
pd.DataFrame(rows).to_csv(csv_path, index=False, encoding="utf-8-sig")
print(f"Predictions saved to: {csv_path}")
后续自己验证即可
解读疑难杂症
经典推导。它在说明:如果我们有一个“好”的训练数据集,那么在训练集上学到的“最好”的模型,在真实世界中也会“几乎”是最好的。
我们用一个“考试”的例子来解读这些数字。
1. 定义和数字实例
想象你是一个学生,准备一场满分100分的大型考试。
-
$D_{all}$ (所有数据):代表“题库”,包含了这场考试可能出现的所有题目(比如10,000道题)。
-
$D_{train}$ (训练数据):代表你的“练习册”,它只包含了题库中的一小部分题目(比如500道题)。
-
$h$ (模型/假设):代表你的一种“学习方法”(比如 $h_1$=“死记硬背”,$h_2$=“理解概念”)。
-
$L(h, D)$ (损失/错误):代表你用 $h$ 方法在 $D$ 数据集上的“错误率”。
-
例如:$L(h, D_{train}) = 10\%$,意思是用 $h$ 方法做“练习册”,错误率是10%(得了90分)。
-
例如:$L(h, D_{all}) = 12\%$,意思是用 $h$ 方法去考“题库”,真实错误率是12%(真实水平是88分)。
-
-
$\delta$ (Delta/容忍度):一个你能接受的“差距”。我们就设 $\delta = 2\%$。
2. 我们的目标 (What do we want?)
$$L(h^{train}, D{all}) - L(h^{all}, D{all}) \le \delta$$
-
$h^{all}$:这是“最强学神”。TA用最牛的方法,在“题库”($D_{all}$)上能达到的最低错误率。
-
假设“最强学神”的真实错误率是 $L(h^{all}, D_{all}) = 5\%$ (真实水平95分)。这是理论上的天花板。
-
-
$h^{train}$:这是“你”。你是根据“练习册”($D_{train}$)学到的最好方法。
-
假设你的练习册上有两种方法可选:
-
方法1(死记硬背)在练习册上错误率 10%。
-
方法2(理解概念)在练习册上错误率 8%。
-
-
你会选择错误率最低的方法2,所以 $h^{train}$ = “理解概念”,并且 $L(h^{train}, D_{train}) = 8\%$。
-
-
$L(h^{train}, D_{all})$:这是你的“真实水平”。也就是用你学到的方法(理解概念)去考“题库”的真实错误率。这是我们最关心的,但也是未知的。
目标的简单解读:
我们希望 “你的真实水平” - “最强学神的水平” $\le 2\%$。
我们希望 $L(h^{train}, D_{all}) - 5\% \le 2\%$。
换句话说,我们的目标是让我们的真实水平(错误率)$L(h^{train}, D_{all}) \le 7\%$。
我们希望我们通过“练习册”学到的本事,和“最强学神”的本事,差距不能超过2%。
3. 如何实现目标?(What kind of $D_{train}$ fulfil it?)
要实现这个目标,我们需要一个什么样“练习册”($D_{train}$)呢?
答案是:这本练习册必须质量非常高,非常有代表性。
条件 (Condition):
$$|L(h, D{train}) - L(h, D{all})| \le \delta/2$$
-
$\delta/2$:既然 $\delta = 2\%$,那么 $\delta/2 = 1\%$。
-
$|...|$:代表“差距的绝对值”。
-
$L(h, D_{train})$:用 任意一种 方法 $h$ 在“练习册”上的错误率。
-
$L(h, D_{all})$:用 同一种 方法 $h$ 在“题库”上的真实错误率。
条件的简单解读:
这个条件要求我们的“练习册”($D_{train}$)必须好到这种程度:无论你用什么学习方法 ( $\forall h$ ),你在“练习册”上的错误率,和你去考“题库”的真实错误率,差距都不能超过 1%!
这意味着,你的练习册(训练数据)必须是“题库”(所有数据)的完美缩影,不能有偏差。
4. 推导过程 (The proof)
现在,我们假设我们的“练习册”真的满足了上面那个1%的苛刻条件,看看我们是否能达成2%的最终目标。
我们来一步步分解这个不等式链条:
第1步:$L(h^{train}, D_{all})$
-
这是我们最关心的“你的真实错误率”。
第2步:$\le L(h^{train}, D_{train}) + \delta/2$
-
依据:根据“条件”,$L(h^{train}, D{all}) - L(h^{train}, D{train}) \le \delta/2$。
-
数字解读:“你的真实错误率” $\le$ “你在练习册上的错误率” + $1\%$。
-
我们已知 $L(h^{train}, D_{train}) = 8\%$ (你在练习册上表现最好)。
-
所以:$L(h^{train}, D_{all}) \le 8\% + 1\% = 9\%$。
第3步:$\le L(h^{all}, D_{train}) + \delta/2$
-
依据:$h^{train}$ (你) 是在“练习册”上表现最好的 ($L(h^{train}, D_{train})$ 最小)。所以你在练习册上的错误率,肯定 $\le$ “最强学神”在 这本练习册 上的错误率。
-
数字解读:$L(h^{train}, D{train}) \le L(h^{all}, D{train})$。
-
你的练习错误率 (8%) $\le$ 学神做你这本练习册的错误率。
-
(比如,学神的“真实水平”是5%错误率,但对这本练习册,他可能刚好错了6%)。
-
所以:$8\% \le 6\%$ 是不成立的。我们换个数字。
-
假设 $h^{all}$ (学神方法) 在 $D{train}$ (练习册) 上的错误率是 **$L(h^{all}, D{train}) = 5.5\%$**。(这完全可能,因为 $h^{train}$ 才是为 $D_{train}$ 优化的)。
-
而你的 $h^{train}$ 在 $D{train}$ 上的错误率是 **$L(h^{train}, D{train}) = 4.5\%$**。(你更适应练习册)
-
-
我们重新代入第2步和第3步:
-
$L(h^{train}, D{all}) \le L(h^{train}, D{train}) + \delta/2 = 4.5\% + 1\% = 5.5\%$
-
$L(h^{train}, D{train}) \le L(h^{all}, D{train})$,所以 $4.5\% \le 5.5\%$ (成立)。
-
因此:$L(h^{train}, D_{all}) \le 5.5\% + 1\% = 6.5\%$。 (这一步是为了把 $h^{train}$ 替换成 $h^{all}$)
-
第4步:$\le L(h^{all}, D_{all}) + \delta/2 + \delta/2$
-
依据:再次使用“条件”,这次用在“最强学神” $h^{all}$ 身上。
-
$L(h^{all}, D{train}) \le L(h^{all}, D{all}) + \delta/2$
-
-
数字解读:“学神在练习册上的错误率” $\le$ “学神的真实错误率” + $1\%$。
-
我们已知 $L(h^{all}, D_{all}) = 5\%$ (学神的真实水平)。
-
$L(h^{all}, D_{train})$ (假设是5.5%) $\le 5\% + 1\% = 6\%$。(这个条件成立)。
-
-
代入第3步的结果:
-
我们刚推到 $\le L(h^{all}, D_{train}) + \delta/2$
-
现在我们把 $L(h^{all}, D_{train})$ 替换掉:
-
$\le [L(h^{all}, D_{all}) + \delta/2] + \delta/2$
-
-
代入数字:
-
$\le [5\% + 1\%] + 1\%$
-
$\le 7\%$
-
第5步:$= L(h^{all}, D_{all}) + \delta$
-
数字解读:$= 5\% + 2\% = 7\%$。
总结
我们从 $L(h^{train}, D_{all})$ (你的真实水平) 出发,通过一步步推导,最终证明了:
$L(h^{train}, D_{all}) \le 7\%$
这正好是我们最初的目标!($L(h^{train}, D{all}) \le L(h^{all}, D{all}) + \delta$)
核心思想:
只要我们能保证我们的“练习册”(训练集 $D_{train}$)质量足够好,能让 任何方法 的“练习分”和“真实分”差距都在1% ($\delta/2$) 以内,那么我们通过这本练习册学到的“最好方法”,其“真实水平”和“学神”的差距一定不会超过2% ($\delta$)。
这张图展示的是图神经网络 (GNN) 中最核心的一步:信息聚合 (Aggregation)。
右下角的中文已经总结了它的目的:“矩阵乘法的意义是为了让节点获取周围节点的信息。这个步骤称之为聚合。”
我们用一个简单的例子来拆解这个计算过程:
1. 定义
-
A (Adjacency matrix - 邻接矩阵):这是一个“谁和谁是邻居”的列表。
-
A[i][j] = 1表示节点i和节点j是邻居。 -
A[i][j] = 0表示它们不是邻居。
-
-
H_0 (Initial Features - 初始信息):这是每个节点一开始拥有的信息。你可以把它想象成每个节点上贴的“数字标签”。
-
节点1的标签是
[1, 1, 1] -
节点2的标签是
[2, 2, 2] -
...
-
节点6的标签是
[6, 6, 6]
-
-
A.matmul(H_0) (矩阵乘法):这就是“聚合”的动作。
2. 用实例计算
我们来看看输出结果 tensor([...]) 的第一行 [7, 7, 7] 是怎么算出来的。
这一行代表节点1在“聚合”后获得的新信息。
-
找到节点1的邻居:
我们查看邻接矩阵 A 的第一行:[0, 1, 0, 0, 1, 0]。
-
第2个位置是1 (
A[0][1]) -
第5个位置是1 (
A[0][4]) -
这表示:节点1的邻居是节点2和节点5。
-
-
获取邻居的信息:
根据“聚合”的规则,节点1的新信息 = 它所有邻居的旧信息相加。
-
节点2的旧信息是:
H_0[1] = [2, 2, 2] -
节点5的旧信息是:
H_0[4] = [5, 5, 5]
-
-
相加(聚合):
-
节点1的新信息 =
[2, 2, 2]+[5, 5, 5]=[7, 7, 7] -
这和输出结果的第一行完全一致!
-
3. 再看一个例子
我们来算一下输出结果的第二行 [9, 9, 9] (节点2的新信息):
-
找到节点2的邻居:
查看邻接矩阵 A 的第二行:[1, 0, 1, 0, 1, 0]。
-
这表示:节点2的邻居是节点1、节点3和节点5。
-
-
获取邻居的信息:
-
节点1的旧信息:
H_0[0] = [1, 1, 1] -
节点3的旧信息:
H_0[2] = [3, 3, 3] -
节点5的旧信息:
H_0[4] = [5, 5, 5]
-
-
相加(聚合):
-
节点2的新信息 =
[1, 1, 1]+[3, 3, 3]+[5, 5, 5]=[9, 9, 9] -
这也和输出结果的第二行完全一致。
-
总结
A.matmul(H_0) 这个操作,本质上就是让每个节点(每一行)都去“查询”它的邻居是谁(由A矩阵定义),然后把所有邻居的“信息”(由H_0矩阵定义)加起来,作为自己的新信息。
请注意:图中的“Labelled graph”与它旁边的“Adjacency matrix”并不完全匹配(例如,图中节点1连接了3个邻居,但矩阵A的第一行显示它只有2个邻居)。这个计算是严格按照矩阵 A 和 H_0 来执行的,图示可能只是一个概念上的说明。
这张图详细展示了反向传播(Backpropagation)的核心——求参数梯度。它以一个非常简单的神经网络为例,一步一步计算出了损失函数 $J$ 对参数 $w_{11}$ 的偏导数(梯度)。
以下是这张图的详细解读:
1. 神经网络模型(图的上半部分)
这是一个两层的前馈神经网络:
-
输入 (Input):$x_1$
-
第一层(隐藏层):
-
输入 $x_1$ 乘以权重 $w_{11}$ 再加上偏置 $b_1$。
-
通过一个Sigmoid激活函数,得到输出 $a_{11}$。
-
$a{11} = \text{sigmoid}(x_1 w{11} + b_1)$
-
-
第二层(输出层):
-
第一层的输出 $a{11}$ 乘以权重 $w{21}$ 再加上偏置 $b_2$。
-
通过一个ReLU激活函数,得到输出 $a_{21}$。
-
$a{21} = \text{relu}(a{11} w_{21} + b_2)$
-
-
预测值 (Prediction):$y$
-
$y = a{21}$ (网络的最终预测值就是 $a{21}$)
-
-
损失函数 (Loss Function):$J(W, b)$
-
根据推导的第三行 $\frac{\partial}{\partial y} (\frac{1}{2}(y-2)^2)$,我们可以看出损失函数是均方误差 (MSE) 的一半。
-
$J = \frac{1}{2}(y{\text{pred}} - y{\text{true}})^2 = \frac{1}{2}(y - 2)^2$
-
这里的真实标签 (true value) 是 $2$。
-
2. 目标:求 $w_{11}$ 的梯度
我们的目标是计算 $\frac{\partial J}{\partial w_{11}}$。
这个值告诉我们:$w_{11}$ 这个参数变化一点点,最终的损失 $J$ 会变化多少? 知道这个值后,我们才能使用梯度下降法来更新 $w_{11}$,以减小损失。
3. 推导过程:链式法则(Chain Rule)
反向传播的本质就是链式法则。我们要从最后(损失 $J$)一直“追溯”到最前(参数 $w_{11}$):
第 1-2 行:建立链式法则
$$\frac{\partial J}{\partial w{11}} = \frac{\partial J}{\partial y} \cdot \frac{\partial y}{\partial a{21}} \cdot \frac{\partial a{21}}{\partial a{11}} \cdot \frac{\partial a{11}}{\partial w{11}}$$
-
$\frac{\partial J}{\partial y}$:损失 $J$ 对预测 $y$ 的偏导。
-
$\frac{\partial y}{\partial a_{21}}$:预测 $y$ 对 $a_{21}$ 的偏导。
-
$\frac{\partial a{21}}{\partial a{11}}$:$a{21}$ 对 $a{11}$ 的偏导。
-
$\frac{\partial a{11}}{\partial w{11}}$:$a{11}$ 对 $w{11}$ 的偏导。
第 3-4 行:分步计算
-
$\frac{\partial J}{\partial y}$ = $\frac{\partial}{\partial y} (\frac{1}{2}(y - 2)^2)$ = $(y - 2)$
-
$\frac{\partial y}{\partial a_{21}}$ = $\frac{\partial a{21}}{\partial a{21}}$ (因为 $y = a_{21}$) = $1$
-
$\frac{\partial a{21}}{\partial a{11}}$ = $\frac{\partial \text{relu}(a{11} w{21} + b_2)}{\partial a_{11}}$
-
ReLU的导数:如果输入 $> 0$,导数为 $1$;如果输入 $\le 0$,导数为 $0$。
-
从第6行 $y = 1.731$ 可知 $a_{21} > 0$,所以ReLU的导数为 $1$。
-
$\frac{\partial a{21}}{\partial a{11}} = 1 \cdot \frac{\partial (a{11} w{21} + b_2)}{\partial a{11}}$ = **$w{21}$**
-
-
$\frac{\partial a{11}}{\partial w{11}}$ = $\frac{\partial \text{sigmoid}(x_1 w{11} + b_1)}{\partial w{11}}$
-
Sigmoid的导数:$\text{sigmoid}'(z) = \text{sigmoid}(z) \cdot (1 - \text{sigmoid}(z))$
-
$\frac{\partial a{11}}{\partial w{11}} = \text{sigmoid}'(x_1 w{11} + b_1) \cdot \frac{\partial (x_1 w{11} + b_1)}{\partial w_{11}}$
-
= $[\text{sigmoid}(x_1 w{11} + b_1) \cdot (1 - \text{sigmoid}(x_1 w{11} + b_1))] \cdot x_1$
-
因为 $a{11} = \text{sigmoid}(x_1 w{11} + b_1)$,所以这也等于 $a{11} \cdot (1 - a{11}) \cdot x_1$
-
第 5 行:组合所有导数
把上面计算的每一项乘起来:
$\frac{\partial J}{\partial w{11}} = (y - 2) \times w{21} \times \text{sigmoid}(x_1 w{11} + b_1) \cdot (1 - \text{sigmoid}(x_1 w{11} + b_1)) \cdot x_1$
第 6 行:代入“前向传播”的数值
在进行反向传播之前,必须先进行一次前向传播(forward pass)来得到所有中间值。这张图直接给出了这些值:
-
$x_1 = 1$ (输入)
-
$a{11} = \text{sigmoid}(x_1 w{11} + b_1) = 0.731$ (第一层输出)
-
$w_{21} = 1$ (第二层权重)
-
$y = a_{21} = 1.731$ (最终预测值)
-
真实值 = $2$
代入:
$\frac{\partial J}{\partial w_{11}} = (1.731 - 2) \times 1 \times 0.731 \times (1 - 0.731) \times 1$
第 7-8 行:最终计算
$\frac{\partial J}{\partial w_{11}} = (-0.269) \times 0.731 \times (0.269) \times 1$
$\frac{\partial J}{\partial w_{11}} = -0.052895$
结果的意义:
这个负值 $-0.052895$ 意味着,如果增加 $w{11}$,损失 $J$ 会减少。因此,在梯度下降更新时, $w{11}$ 应该被增加。
(公式:$w{\text{new}} = w{\text{old}} - \eta \cdot \text{gradient}$,这里 $w{\text{new}} = w{11} - \eta \cdot (-0.052895)$,变成了加法)。
4. 关于 $b_1$ 的梯度
$\frac{\partial J(b_1)}{\partial b_1} = -0.052895$
“同理可得”,$b_1$ 的梯度和 $w_{11}$ 的梯度在数值上完全一样。为什么?
-
$b_1$ 的链式法则是:$\frac{\partial J}{\partial b_1} = \frac{\partial J}{\partial y} \cdot \frac{\partial y}{\partial a{21}} \cdot \frac{\partial a{21}}{\partial a{11}} \cdot \frac{\partial a{11}}{\partial b_1}$
-
前三项都和 $w_{11}$ 的计算完全相同。
-
最后一项 $\frac{\partial a{11}}{\partial b_1} = \frac{\partial \text{sigmoid}(x_1 w{11} + b_1)}{\partial b_1} = [\text{sigmoid}(\dots) \cdot (1 - \text{sigmoid}(\dots))] \cdot 1$
-
而 $\frac{\partial a{11}}{\partial w{11}} = [\text{sigmoid}(\dots) \cdot (1 - \text{sigmoid}(\dots))] \cdot x_1$
唯一的区别是 $\frac{\partial a{11}}{\partial b_1}$ 乘以 $1$,而 $\frac{\partial a{11}}{\partial w_{11}}$ 乘以 $x_1$。
因为在这次计算中,输入 $x_1 = 1$,所以两个导数最终的计算结果完全相同。
这张图是在解释一个著名的数学问题:如何构建一个“2元2级德布鲁因(De Bruijn)序列”。
以下是详细解读:
1. 提问(左侧)
-
问题:在一个有4个扇区的转盘上,放入两个0和两个1。当你顺时针转动转盘时,你有一个“观察窗”始终只能看到相邻的两位数。如何摆放这四个数,才能让你每转动一次,看到的两位数都完全不一样?
-
目标:4次转动(包括起始位置),需要分别看到 00, 01, 10, 11 这四种所有可能的两位二进制数,且每种只出现一次。
2. 解决方案(右侧的图)
右侧的图叫作德布鲁因图(De Bruijn graph),它是解决这个问题的钥匙。
-
节点 (Nodes):图中有两个节点,分别是 0 和 1。
-
在“2元2级”问题中,节点代表所有可能的 $N-1$ 位数。这里 $N=2$,所以节点代表所有可能的1位数,即 "0" 和 "1"。
-
-
有向边 (Edges):图中有四条带箭头的边,分别是 00, 01, 10, 11。
-
这些边代表了我们要寻找的所有 $N$ 位数(这里是2位数)。
-
边的“起点”和“终点”由节点决定:一条标签为
XY的边,会从节点X指向节点Y。 -
例如:
-
00:从 0 指向 0 (自循环)
-
01:从 0 指向 1
-
10:从 1 指向 0
-
11:从 1 指向 1 (自循环)
-
-
3. 如何从图中找到答案
我们的目标是找到一条“欧拉回路”(Eulerian path)—— 这是一条“一笔画”的路径,它必须走过所有的4条边,且每条边只走一次,最后回到起点。
这个路径上你访问的节点的顺序(或者边的第一个数字的顺序)就是转盘上数字的排列方式。
让我们来“走”一遍:
-
起点:我们从节点 0 开始。
-
第1步:走 00 这条边。
-
路径:0 → 0
-
我们得到的序列的第一个数是 0。
-
我们看到了 00。
-
-
第2步:现在我们在节点 0,走 01 这条边。
-
路径:0 → 1
-
我们得到的序列的第二个数是 0。
-
我们看到了 01。
-
-
第3步:现在我们在节点 1,走 11 这条边。
-
路径:1 → 1
-
我们得到的序列的第三个数是 1。
-
我们看到了 11。
-
-
第4步:现在我们在节点 1,走 10 这条边。
-
路径:1 → 0
-
我们得到的序列的第四个数是 1。
-
我们看到了 10。
-
-
结束:我们回到了起点 0,并且所有4条边都走完了。
最终答案:
我们按顺序走的边的第一个数字组合起来,就是这个序列:0, 0, 1, 1。
验证:
把 0 0 1 1 顺时针放在转盘上。
-
观察窗看到:00
-
转一下,看到:01
-
再转一下,看到:11
-
再转一下(从最后的1绕回开头的0),看到:10
所有四种组合都看到了,问题解决!
(P.S. 存在另一个解:0110。你可以试试从节点0出发,先走01这条边,也能找到这个解。)
文献阅读
使用 RNAhybrid 软件,通过计算“结合能”来预测 miRNA 会“粘”在靶基因的哪个位置。
当然可以。这张图的核心意思是:使用 RNAhybrid 软件,通过计算“结合能”来预测 miRNA 会“粘”在靶基因的哪个位置。
我们来举一个实例,一步一步解读这个预测过程。
1. 预测的两个主角:miRNA 和靶基因
首先,你需要两个RNA序列(用A, U, G, C表示):
-
miRNA 序列 (短):
-
这是一条很短的RNA,大约22个碱基长。我们虚构一个叫
miR-123的miRNA。 -
miR-123序列:5'-AUGUACUGAGCUAGUACUGGA-3'
-
-
靶基因 mRNA 序列 (长):
-
这就是图中的“靶基因 E75”。miRNA通常结合在基因的3'UTR区域,所以我们拿E75基因的3'UTR序列(或像图中提到的编码区)作为靶标。这是一个非常长的序列,可能有几千个碱基。
-
E75序列 (片段):...GCAUUCAUGAUC**AUGAUGA**CCAU...
-
2. 预测的核心原理:寻找“最速配”的结合点
RNAhybrid 软件的工作就像一个“自动配对”程序。它会拿着短的 miR-123 序列,在长的 E75 序列上一格一格地滑动匹配,寻找“粘”得最牢固的区域。
这个“牢固”程度主要由两个规则决定:
规则一:寻找“种子区域” (Seed Region)
-
miRNA的第2到第8个碱基(从5'端开始数)被称为“种子区域”。
-
miR-123的种子区域:5'-A**UGUACUG**AGCUAGUACUGGA-3'(即UGUACUG) -
软件会优先寻找
E75序列上能与这个“种子区域”完美反向互补的地方。(A配U,G配C) -
miR-123种子:UGUACUG -
完美互补序列:
ACAUGA C -
软件在 E75 序列上扫描,发现了一个非常匹配的区域!
...GCAUUCAUGAUCAUGAUGACCAU...
这里的 AUGAUGA 和 ACAUGA C 非常接近。
规则二:计算“最小自由能” (Minimum Free Energy, MFE)
-
一旦找到了一个不错的“种子”匹配点,
RNAhybrid就会计算整个miRNA短链与该区域结合的总能量。 -
RNA的碱基配对会释放能量。配对越完美(A-U, G-C),释放的能量越多,结构越稳定。
-
这种稳定性能量值用“自由能”表示,单位是
kcal/mol。这个值越负(比如 -30 kcal/mol),代表结合越稳定,越牢固。 -
“错配”(比如A和C相对)或“凸起”(一个链上有碱基,另一链上没有)会增加能量,使结合不稳定。
3. 预测结果:一个实例
RNAhybrid 软件会输出一个类似这样的结果,告诉你“最佳结合点”在哪里:
靶标: E75基因 (位置 310-335) miRNA: miR-123 靶标 5' ...AUUCAUGAUCAUGAUGACCAU... 3' ||||| | ||||||| || miRNA 3' AGGUCGUAGCUAGUCAUGUA... 5' (种子区) 能量 (MFE): -28.5 kcal/mol
解读这个结果:
-
位置:软件告诉你,在
E75基因序列的第310到335个碱基位置,找到了一个最好的结合点。 -
配对:
-
|竖线代表完美的 G-C 或 A-U 配对。 -
:(图中未显示,但很常见)代表稍弱的 G-U 配对。 -
空格代表“错配”或“凸起”。
-
-
种子区域:你可以看到,在miRNA的种子区域(右侧),几乎是完美配对的。这是结合的核心。
-
能量值 (-28.5 kcal/mol):这是关键数字。这个值足够“负”,表明这是一个非常稳定、非常可能的结合位点。
总结
就像您图中所说,研究人员就是用 RNAhybrid 做了这个计算。
-
他们输入了“靶基因 E75”的完整序列。
-
他们输入了三个不同的 miRNA 序列。
-
软件分别计算了这三个 miRNA 在 E75 上的所有可能结合点,并返回了能量最低(最稳定)的那几个。
-
最终,他们“初步确定了三个miRNA与靶基因E75”的结合位点,因为这些位点的计算能量值很低(比如低于-25 kcal/mol),并且具有良好的“种子区域”配对。
cora数据集做图神经网络
# -*- coding: utf-8 -*-
import os, random
import numpy as np
import matplotlib.pyplot as plt
import networkx as nx
import torch
from torch_geometric.datasets import Planetoid
import torch_geometric.transforms as T
# ====== 配置 ======
MODEL_NAME = "gcn" # 你也可以改成 "gat"/"sage",对应前面保存的 pred_gat.npy / pred_sage.npy
OUT_DIR = "cora_viz_outputs"
DRAW_MODE = "ego" # "full"=整图 | "sample"=随机子图 | "ego"=k-hop 自中心子图
SUBSET_N = 600 # 随机采样子图尺寸(DRAW_MODE="sample" 时生效)
EGO_CENTER = None # 指定中心节点 id(None=自动选一个度最高或随机的)
EGO_K = 2 # k-hop 半径
SEED = 42
random.seed(SEED); np.random.seed(SEED)
# ====== 载入数据与预测 ======
dataset = Planetoid(root="Planetoid", name="Cora", transform=T.NormalizeFeatures())
data = dataset[0]
y_true = data.y.numpy()
test_mask = data.test_mask.numpy()
pred_path = os.path.join(OUT_DIR, f"pred_{MODEL_NAME}.npy")
if not os.path.exists(pred_path):
raise FileNotFoundError(f"未找到预测文件:{pred_path},请先跑 cora_gnn_viz_all.py 生成。")
y_pred = np.load(pred_path) # 全图的预测
# ====== 构图(PyG -> NetworkX)======
edge_index = data.edge_index.numpy()
G = nx.Graph()
G.add_nodes_from(range(data.num_nodes))
edges = list(zip(edge_index[0], edge_index[1]))
# Cora 是无向图,去重
G.add_edges_from((u, v) for u, v in edges if u != v)
# ====== 选择子图 ======
if DRAW_MODE == "full":
H = G
title_suffix = " (full graph)"
elif DRAW_MODE == "sample":
# 随机选 SUBSET_N 个节点诱导子图
nodes = np.random.choice(G.number_of_nodes(), size=min(SUBSET_N, G.number_of_nodes()), replace=False)
H = G.subgraph(nodes).copy()
title_suffix = f" (random {H.number_of_nodes()} nodes)"
else:
# ego:选择一个中心节点(默认:度最大的测试集节点;否则随机一个测试集节点)
if EGO_CENTER is None:
# 选测试集里度最大的节点当中心;若找不到就随机一个
test_nodes = np.where(test_mask)[0]
if len(test_nodes) == 0:
EGO_CENTER = random.randrange(G.number_of_nodes())
else:
center_deg_pairs = [(n, G.degree[n]) for n in test_nodes]
center_deg_pairs.sort(key=lambda x: x[1], reverse=True)
EGO_CENTER = center_deg_pairs[0][0]
ego_nodes = nx.ego_graph(G, EGO_CENTER, radius=EGO_K).nodes()
H = G.subgraph(ego_nodes).copy()
title_suffix = f" (ego k={EGO_K}, center={EGO_CENTER}, |V|={H.number_of_nodes()})"
print(f"Subgraph: |V|={H.number_of_nodes()}, |E|={H.number_of_edges()}")
# ====== 准备颜色映射 ======
# 方案 A:按“是否预测正确”着色(正确=绿,错误=红),非测试集=灰
colors = []
for n in H.nodes():
if test_mask[n]:
colors.append("#2ca02c" if y_pred[n] == y_true[n] else "#d62728")
else:
colors.append("#bbbbbb")
# 方案 B(可选):按真实类别着色
# 取消下方注释即可用类别调色
# import matplotlib.cm as cm
# cmap = cm.get_cmap('tab10', dataset.num_classes) # 7 类
# colors = [cmap(y_true[n]) for n in H.nodes()]
# 节点大小:测试集更大,非测试集更小(视觉区分)
sizes = [30 if test_mask[n] else 10 for n in H.nodes()]
# ====== 布局(spring 或 kamada_kawai,节点多时 spring 更稳)======
if H.number_of_nodes() <= 1200:
pos = nx.spring_layout(H, seed=SEED, k=None)
else:
pos = nx.spring_layout(H, seed=SEED, k=0.05) # 大图适当调弹簧长度
# ====== 绘图 ======
plt.figure(figsize=(10, 8))
nx.draw_networkx_nodes(H, pos, node_color=colors, node_size=sizes, linewidths=0.0)
nx.draw_networkx_edges(H, pos, width=0.5, alpha=0.25)
plt.axis("off")
plt.title(f"Cora network{title_suffix}\nGreen=Correct (test), Red=Wrong (test), Gray=Non-test", fontsize=12)
# 保存与展示
os.makedirs(OUT_DIR, exist_ok=True)
out_name = f"cora_net_{MODEL_NAME}_{DRAW_MODE}.png"
plt.tight_layout()
plt.savefig(os.path.join(OUT_DIR, out_name), dpi=300)
plt.show()
print(f"[Saved] {os.path.join(OUT_DIR, out_name)}")
更多推荐



所有评论(0)