对比学习中的NCE(Noise-Contrastive Estimation)和InfoNCE(SimCLR)损失函数+案例(附SimSiam分析)
在对比学习(Contrastive Learning)中,NCE(Noise-Contrastive Estimation)和InfoNCE是两种常见的目标函数,它们都用于通过区分正样本和负样本来学习高质量的表示。
1. NCE(Noise-Contrastive Estimation)
- 定义:NCE最初是为无监督学习中的概率模型(如语言模型)设计的,目的是通过对比正样本和噪声样本(负样本)来估计概率分布。它将任务转化为一个二分类问题:区分真实数据(正样本)和噪声分布生成的样本(负样本)。
- 目标:最大化正样本的对数似然,同时最小化负样本的对数似然。形式上,NCE的目标函数可以写为:
L N C E = E p data ( x ) [ log σ ( f ( x ) ) ] + k ⋅ E p noise ( x ) [ log ( 1 − σ ( f ( x ) ) ) ] L_{NCE} = \mathbb{E}_{p_{\text{data}}(x)}[\log \sigma(f(x))] + k \cdot \mathbb{E}_{p_{\text{noise}}(x)}[\log (1 - \sigma(f(x)))] LNCE=Epdata(x)[logσ(f(x))]+k⋅Epnoise(x)[log(1−σ(f(x)))]
其中:- p data ( x ) p_{\text{data}}(x) pdata(x) 是真实数据分布;
- p noise ( x ) p_{\text{noise}}(x) pnoise(x) 是噪声分布;
- f ( x ) f(x) f(x) 是模型的评分函数(如神经网络输出);
- σ \sigma σ 是sigmoid函数;
- k k k 是负样本的数量。
- 核心思想:通过对比正样本和负样本,逼近真实数据分布的似然估计。负样本通常从一个预定义的噪声分布(如均匀分布或高斯分布)中采样。
- 特点:
- 更偏向于概率建模,适用于估计数据分布。
- 负样本的生成依赖于噪声分布的选择,质量和多样性可能受限。
- 计算复杂度与负样本数量成正比。
2. InfoNCE
- 定义:InfoNCE是NCE的一种变体,全称是“Information Noise-Contrastive Estimation”,广泛用于现代对比学习框架(如SimCLR、MoCo等)。它基于互信息(Mutual Information)的最大化,通过对比正样本和一组负样本学习表示。
- 目标:InfoNCE的目标是最大化正样本对的相似度,同时最小化与负样本的相似度。其形式通常为:
L I n f o N C E = − E [ log exp ( s ( x , x + ) / τ ) exp ( s ( x , x + ) / τ ) + ∑ i = 1 N exp ( s ( x , x i − ) / τ ) ] L_{InfoNCE} = -\mathbb{E} \left[ \log \frac{\exp(s(x, x^+)/\tau)}{\exp(s(x, x^+)/\tau) + \sum_{i=1}^{N} \exp(s(x, x_i^-)/\tau)} \right] LInfoNCE=−E[logexp(s(x,x+)/τ)+∑i=1Nexp(s(x,xi−)/τ)exp(s(x,x+)/τ)]
其中:- x x x 是锚点样本, x + x^+ x+ 是正样本, x i − x_i^- xi− 是负样本;
- s ( ⋅ , ⋅ ) s(\cdot, \cdot) s(⋅,⋅) 是相似度函数(如余弦相似度或点积);
- τ \tau τ 是温度参数,用于控制分布的平滑性;
- N N N 是负样本数量。
- 核心思想:通过softmax形式的分类任务,最大化锚点与正样本之间的互信息下界。负样本通常来自同一批次的数据(而不是预定义的噪声分布)。
- 特点:
- 更适合表示学习(representation learning),而非直接建模概率分布。
- 负样本通常从数据本身采样(如数据增强或批次中的其他样本),更贴近任务分布。
- 引入温度参数 τ \tau τ,可以调节模型对相似度的敏感度。
主要区别
方面 | NCE | InfoNCE |
---|---|---|
设计初衷 | 概率分布估计(如语言模型) | 表示学习(如图像、视频表征) |
负样本来源 | 预定义噪声分布(如均匀或高斯) | 数据本身(如批次内其他样本) |
目标 | 逼近似然估计 | 最大化互信息下界 |
数学形式 | 二分类形式(sigmoid) | 多分类形式(softmax) |
温度参数 | 无 | 有( τ \tau τ,控制分布平滑性) |
应用场景 | 传统无监督学习(如word2vec) | 现代对比学习(如SimCLR、MoCo) |
负样本数量 | 固定或较少 | 通常较多(如批次大小决定) |
联系与演化
- 联系:InfoNCE可以看作是NCE的扩展和改进。两者都基于对比的思想,即通过区分正样本和负样本优化模型。
- 演化:NCE更早提出,适用于概率建模任务;InfoNCE在深度学习时代被改进,结合了互信息的理论基础,更适合表示学习任务,尤其是在大规模数据和神经网络中。
- 如果任务是估计概率分布(如生成模型),NCE可能更合适。
- 如果目标是学习数据表示(如自监督学习中的特征提取),InfoNCE是更好的选择,因为它更灵活且与现代深度学习框架兼容性更高。
案例
“NCE 是 word2vec 中用于高效训练词嵌入的关键技术,它通过将多分类问题转化为二分类问题来避免 softmax 计算中的归一化项,大幅提高训练效率。”
假设你在找朋友“猫”常去的咖啡店:
- 原始方法(softmax):你得跑遍城里 10 万家咖啡店,算每家店“猫”去的可能性,最后挑出最可能的。
- NCE 方法:你只去“猫”常去的 1 家店(正样本),再随便挑 5 家“猫”从不去的店(负样本),问:“猫在这儿吗?”判断完就行了。
1. 什么是 word2vec 和词嵌入?
- 词嵌入:简单来说,就是把单词变成一串数字(向量),让计算机能理解。比如“猫”可能是 [0.1, 0.5, -0.2],“狗”可能是 [0.2, 0.4, -0.1],相似的词向量会更接近。
- word2vec:一个工具,用来从大量文本中学习这些词向量。它的工作方式是:给一个词(比如“猫”),预测它周围的词(比如“喵”或“宠物”)。
2. 为什么训练 word2vec 会有问题?
想象你在教一个模型预测“猫”旁边的词可能是“喵”。模型需要:
- 看整个词典(比如 10 万个词)。
- 给每个词打分,算出“喵”的概率最高。
- 用一个叫 softmax 的公式,把所有词的得分变成概率。
问题来了:如果词典有 10 万个词,每次预测都要算 10 万次得分,再把它们加起来(这就是“归一化项”),太慢了!就像你每次点餐都要把菜单上的 10 万道菜全看一遍,太费时间。
3. NCE 是什么?它怎么解决问题?
NCE(Noise-Contrastive Estimation,噪声对比估计)是 word2vec 的一个“加速器”。它不让你看整个菜单,而是用一个聪明的方法:
- 原来的任务(多分类):从 10 万个词里挑出“喵”作为“猫”的邻居。
- NCE 的新任务(二分类):只问一个简单问题:“这个词(比如‘喵’)是‘猫’的邻居吗?是/不是。”然后再随机挑几个“假邻居”(比如“桌子”“飞机”),问:“这些是‘猫’的邻居吗?是/不是。”
类比:就像考试从 10 万道选择题变成几道判断题。你不用算所有词的概率,只需要判断几个词的对错,简单多了。
4. “避免 softmax 计算中的归一化项”是什么意思?
- softmax:本来要算所有 10 万个词的得分总和(归一化项),才能知道“喵”的概率是多少。
- NCE:不算总和了!它只看“喵”和几个假词(比如“桌子”“飞机”),直接比较它们和“猫”的匹配度。这样就不用把 10 万个词加起来,省了很多计算。
举个例子:
- 原始方法:算“喵”“狗”“桌子”“飞机”……10 万个词的得分,再加起来,得出“喵”的概率。
- NCE 方法:只算“喵”和 5 个随机假词(比如“桌子”“飞机”),判断“喵”是真的邻居,其他是假的。
5. “大幅提高训练效率”
- 直观理解:原来算 10 万次,现在只算 6 次(1 个真词 + 5 个假词),速度快了几万倍!
- 结果:模型训练从几天变成几小时,甚至更快,能处理更大的词典和文本。
人工生成语料库代码解释
word_probs = 1.0 / torch.arange(1, vocab_size + 1, dtype=torch.float) ** 0.75
word_probs /= word_probs.sum()
corpus = torch.multinomial(word_probs, corpus_size, replacement=True)
1. word_probs = 1.0 / torch.arange(1, vocab_size + 1, dtype=torch.float) ** 0.75
- 做什么:这一行创建了一个词的“概率分布”,但不是均匀分布,而是根据词的排名(频率)调整的。
- 通俗理解:想象你有一个词典(比如 5 个词),每个词按频率排名:第 1 名最常见,第 5 名最不常见。这里用一个公式(倒数幂次)给每个词分配一个初始“重要性”分数,排名靠前的词分数更高,但差距不会太大。
- 细节:
torch.arange(1, vocab_size + 1, dtype=torch.float)
:生成一个从 1 到vocab_size
的序列,比如词典有 5 个词,就是[1, 2, 3, 4, 5]
。- ∗ ∗ 0.75 ** 0.75 ∗∗0.75:对每个数字取 0.75 次幂,比如 1 0.75 = 1 1^{0.75} = 1 10.75=1, 2 0.75 ≈ 1.68 2^{0.75} \approx 1.68 20.75≈1.68,这会让数字增长变慢,拉近高低排名的差距。
- 1.0 / . . . 1.0 / ... 1.0/...:取倒数,比如 $ 1/1 = 1 , , , 1/1.68 \approx 0.595 $,排名越靠后,分数越低。
2. word_probs /= word_probs.sum()
- 做什么:把上一行的分数变成真正的概率,确保它们加起来等于 1。
- 通俗理解:就像你把一堆筹码分给不同的人,但总筹码数要固定为 1。上一行算出的分数只是相对大小,这里把它们“标准化”,变成概率。
- 细节:用总和除以每个分数,保证是个合法的概率分布。
3. corpus = torch.multinomial(word_probs, corpus_size, replacement=True)
- 做什么:根据刚刚算出的概率,随机生成一个语料库(corpus),里面是词的索引。
- 通俗理解:想象一个抽奖转盘,每个词占一块区域,概率高的词区域大,被抽中的机会多。这里根据
word_probs
的概率,抽corpus_size
次,生成一串词的编号。 - 细节:
torch.multinomial
:一个抽样函数,按概率抽取。replacement=True
:表示“有放回”抽样,同一个词可以被重复抽中。- 输出是一个长度为
corpus_size
的张量,里面是词的索引(0 到vocab_size-1
)。
具体例子
假设我们设置:
vocab_size = 5
(词典里有 5 个词:[“猫”, “狗”, “鱼”, “鸟”, “鼠”])corpus_size = 10
(想生成一个 10 个词的语料库)
步骤 1:计算初始概率
word_probs = 1.0 / torch.arange(1, 6, dtype=torch.float) ** 0.75
torch.arange(1, 6)
生成[1, 2, 3, 4, 5]
。- 每个数取 0.75 次幂:
- 1 0.75 = 1 1^{0.75} = 1 10.75=1
- 2 0.75 ≈ 1.6818 2^{0.75} \approx 1.6818 20.75≈1.6818
- 3 0.75 ≈ 2.2795 3^{0.75} \approx 2.2795 30.75≈2.2795
- 4 0.75 ≈ 2.8284 4^{0.75} \approx 2.8284 40.75≈2.8284
- 5 0.75 ≈ 3.3437 5^{0.75} \approx 3.3437 50.75≈3.3437
- 取倒数:
- 1 / 1 = 1 1/1 = 1 1/1=1
- 1 / 1.6818 ≈ 0.5946 1/1.6818 \approx 0.5946 1/1.6818≈0.5946
- 1 / 2.2795 ≈ 0.4386 1/2.2795 \approx 0.4386 1/2.2795≈0.4386
- 1 / 2.8284 ≈ 0.3536 1/2.8284 \approx 0.3536 1/2.8284≈0.3536
- 1 / 3.3437 ≈ 0.2991 1/3.3437 \approx 0.2991 1/3.3437≈0.2991
- 结果:
word_probs = [1, 0.5946, 0.4386, 0.3536, 0.2991]
。
步骤 2:归一化成概率
word_probs /= word_probs.sum()
- 先算总和: 1 + 0.5946 + 0.4386 + 0.3536 + 0.2991 ≈ 2.6859 1 + 0.5946 + 0.4386 + 0.3536 + 0.2991 \approx 2.6859 1+0.5946+0.4386+0.3536+0.2991≈2.6859。
- 归一化:
- 1 / 2.6859 ≈ 0.3724 1 / 2.6859 \approx 0.3724 1/2.6859≈0.3724
- 0.5946 / 2.6859 ≈ 0.2214 0.5946 / 2.6859 \approx 0.2214 0.5946/2.6859≈0.2214
- 0.4386 / 2.6859 ≈ 0.1633 0.4386 / 2.6859 \approx 0.1633 0.4386/2.6859≈0.1633
- 0.3536 / 2.6859 ≈ 0.1317 0.3536 / 2.6859 \approx 0.1317 0.3536/2.6859≈0.1317
- 0.2991 / 2.6859 ≈ 0.1113 0.2991 / 2.6859 \approx 0.1113 0.2991/2.6859≈0.1113
- 结果:
word_probs = [0.3724, 0.2214, 0.1633, 0.1317, 0.1113]
。 - 检查:加起来是 1,符合概率要求。
步骤 3:生成语料库
corpus = torch.multinomial(word_probs, 10, replacement=True)
- 根据概率
[0.3724, 0.2214, 0.1633, 0.1317, 0.1113]
抽 10 次。 - 假设抽到的结果是:
[0, 2, 1, 0, 4, 3, 0, 1, 2, 0]
。 - 对应词:
["猫", "鱼", "狗", "猫", "鼠", "鸟", "猫", "狗", "鱼", "猫"]
。
为什么这样做?
这种方法模仿了自然语言中词频的分布(常见词多,罕见词少),常用于 word2vec 的负采样或语料生成。 0.75 0.75 0.75 次幂是个经验值,能平衡常见词和稀有词的出现频率。
import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np
import matplotlib.pyplot as plt
from tqdm import tqdm
import timeclass ImprovedNCELoss(nn.Module):"""噪声对比估计(Noise Contrastive Estimation, NCE)损失函数实现NCE是word2vec中用于高效训练词嵌入的关键技术,它通过将多分类问题转化为二分类问题来避免softmax计算中的归一化项,大幅提高训练效率。参数:vocab_size (int): 词汇表大小embedding_dim (int): 词嵌入维度num_neg_samples (int): 每个正样本对应的负样本数量noise_distribution (torch.Tensor): 噪声分布,默认为均匀分布"""def __init__(self, vocab_size, embedding_dim, num_neg_samples, noise_distribution=None):super(ImprovedNCELoss, self).__init__()# 初始化词嵌入矩阵self.center_embeddings = nn.Embedding(vocab_size, embedding_dim)self.context_embeddings = nn.Embedding(vocab_size, embedding_dim)# Xavier初始化以改善训练收敛性nn.init.xavier_uniform_(self.center_embeddings.weight)nn.init.xavier_uniform_(self.context_embeddings.weight)self.num_neg_samples = num_neg_samples# 如果没有提供噪声分布,则使用均匀分布if noise_distribution is None:self.register_buffer('noise_distribution', torch.ones(vocab_size))else:# 确保分布和为1noise_distribution = noise_distribution / noise_distribution.sum()self.register_buffer('noise_distribution', noise_distribution)# 数值稳定性参数self.eps = 1e-10def forward(self, center_words, context_words):"""计算NCE损失参数:center_words (torch.Tensor): 形状为[batch_size]的中心词索引context_words (torch.Tensor): 形状为[batch_size]的上下文词索引返回:loss (torch.Tensor): NCE损失值pos_loss (torch.Tensor): 正样本损失neg_loss (torch.Tensor): 负样本损失accuracy (torch.Tensor): 二分类准确率"""batch_size = center_words.size(0)device = center_words.device # 获取输入张量的设备# 获取中心词和上下文词的嵌入表示# [batch_size, embedding_dim]center_embeds = self.center_embeddings(center_words)context_embeds = self.context_embeddings(context_words)# 计算正样本得分: 点积后应用Sigmoid# [batch_size]pos_scores = torch.sum(center_embeds * context_embeds, dim=1)pos_probs = torch.sigmoid(pos_scores)# 从噪声分布中采样负样本# [batch_size, num_neg_samples]neg_samples = torch.multinomial(self.noise_distribution,batch_size * self.num_neg_samples,replacement=True).view(batch_size, self.num_neg_samples).to(device) # 确保在正确的设备上# 确保负样本不包含正样本for i in range(batch_size):mask = (neg_samples[i] == context_words[i])if mask.any():# 如果存在冲突,随机替换 - 确保替换索引在正确的设备上replacement_indices = torch.randint(0,len(self.noise_distribution),(mask.sum(),),device=device # 确保在与其他张量相同的设备上)neg_samples[i, mask] = replacement_indices# 获取负样本的嵌入表示# [batch_size, num_neg_samples, embedding_dim]neg_embeds = self.context_embeddings(neg_samples)# 计算负样本得分# [batch_size, num_neg_samples]neg_scores = torch.bmm(neg_embeds,center_embeds.unsqueeze(2)).squeeze(2)neg_probs = torch.sigmoid(-neg_scores)# 计算损失# 正样本损失: -log(sigmoid(pos_score))pos_loss = -torch.log(pos_probs + self.eps).mean()# 负样本损失: -log(sigmoid(-neg_score))neg_loss = -torch.log(neg_probs + self.eps).mean()# 总损失loss = pos_loss + neg_loss# 计算分类准确率(用于监控训练进度)with torch.no_grad():pos_correct = (pos_probs > 0.5).float().sum()neg_correct = (neg_probs > 0.5).float().sum()total_samples = batch_size + batch_size * self.num_neg_samplesaccuracy = (pos_correct + neg_correct) / total_samplesreturn loss, pos_loss, neg_loss, accuracydef generate_synthetic_data(vocab_size, corpus_size, window_size=2):"""生成合成语料库及其中心词-上下文对参数:vocab_size (int): 词汇表大小corpus_size (int): 语料库大小(词数)window_size (int): 窗口大小返回:corpus (torch.Tensor): 语料库center_words (torch.Tensor): 中心词索引列表context_words (torch.Tensor): 上下文词索引列表word_freqs (torch.Tensor): 词频分布"""# 生成遵循Zipf分布的随机语料库word_probs = 1.0 / torch.arange(1, vocab_size + 1, dtype=torch.float) ** 0.75word_probs /= word_probs.sum()corpus = torch.multinomial(word_probs, corpus_size, replacement=True)# 准备中心词和上下文词对center_words, context_words = [], []# 对每个位置生成中心词-上下文对for i in range(corpus_size):center_word = corpus[i]# 定义上下文窗口范围context_start = max(0, i - window_size)context_end = min(corpus_size, i + window_size + 1)# 收集上下文词for j in range(context_start, context_end):if i != j: # 排除中心词本身center_words.append(center_word)context_words.append(corpus[j])# 计算词频word_freqs = torch.zeros(vocab_size)for word in corpus:word_freqs[word] += 1word_freqs = word_freqs / word_freqs.sum()return (corpus,torch.tensor(center_words),torch.tensor(context_words),word_freqs)def train_word_embeddings():"""使用NCE损失函数训练词嵌入的完整流程"""# 设置超参数vocab_size = 5000embedding_dim = 100num_neg_samples = 10learning_rate = 0.001batch_size = 512epochs = 50corpus_size = 10000window_size = 2# 选择设备device = torch.device("cuda" if torch.cuda.is_available() else "cpu")print(f"使用设备: {device}")# 生成合成数据print("生成合成语料库...")corpus, center_words, context_words, word_freqs = generate_synthetic_data(vocab_size, corpus_size, window_size)# 将数据移动到正确的设备上corpus = corpus.to(device)center_words = center_words.to(device)context_words = context_words.to(device)word_freqs = word_freqs.to(device)# 创建数据加载器dataset_size = len(center_words)num_batches = (dataset_size - 1) // batch_size + 1# 初始化模型和优化器model = ImprovedNCELoss(vocab_size=vocab_size,embedding_dim=embedding_dim,num_neg_samples=num_neg_samples,noise_distribution=word_freqs ** 0.75 # 使用平滑的词频分布作为噪声分布).to(device)optimizer = optim.Adam(model.parameters(), lr=learning_rate)scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='min', factor=0.5, patience=1)# 记录训练过程training_stats = {'total_loss': [],'pos_loss': [],'neg_loss': [],'accuracy': [],'time_per_epoch': []}print(f"开始训练,共{epochs}轮...")# 训练循环for epoch in range(epochs):epoch_start_time = time.time()epoch_total_loss = 0.0epoch_pos_loss = 0.0epoch_neg_loss = 0.0epoch_accuracy = 0.0# 随机打乱数据indices = torch.randperm(dataset_size, device=device) # 确保在正确的设备上# 使用tqdm创建进度条pbar = tqdm(range(num_batches), desc=f"Epoch {epoch + 1}/{epochs}")for batch_idx in pbar:# 获取当前批次的索引start_idx = batch_idx * batch_sizeend_idx = min((batch_idx + 1) * batch_size, dataset_size)batch_indices = indices[start_idx:end_idx]# 获取中心词和上下文词批次batch_center = center_words[batch_indices]batch_context = context_words[batch_indices]# 前向传播和反向传播optimizer.zero_grad()loss, pos_loss, neg_loss, accuracy = model(batch_center, batch_context)loss.backward()# 梯度裁剪以防止梯度爆炸torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=5.0)optimizer.step()# 累加统计数据batch_size_actual = end_idx - start_idxepoch_total_loss += loss.item() * batch_size_actualepoch_pos_loss += pos_loss.item() * batch_size_actualepoch_neg_loss += neg_loss.item() * batch_size_actualepoch_accuracy += accuracy.item() * batch_size_actual# 更新进度条pbar.set_postfix({'loss': f"{loss.item():.4f}",'acc': f"{accuracy.item():.4f}"})# 计算每轮的平均统计数据epoch_total_loss /= dataset_sizeepoch_pos_loss /= dataset_sizeepoch_neg_loss /= dataset_sizeepoch_accuracy /= dataset_sizeepoch_time = time.time() - epoch_start_time# 保存训练统计数据training_stats['total_loss'].append(epoch_total_loss)training_stats['pos_loss'].append(epoch_pos_loss)training_stats['neg_loss'].append(epoch_neg_loss)training_stats['accuracy'].append(epoch_accuracy)training_stats['time_per_epoch'].append(epoch_time)print(f"Epoch {epoch + 1}/{epochs} - "f"Loss: {epoch_total_loss:.4f} "f"(Pos: {epoch_pos_loss:.4f}, Neg: {epoch_neg_loss:.4f}) - "f"Accuracy: {epoch_accuracy:.4f} - "f"Time: {epoch_time:.2f}s")# 学习率调度scheduler.step(epoch_total_loss)print("训练完成!")# 可视化训练过程visualize_training(training_stats)# 可视化词嵌入visualize_embeddings(model, corpus, 50)return model, training_statsdef visualize_training(stats):"""可视化训练过程参数:stats (dict): 包含训练统计数据的字典"""plt.figure(figsize=(12, 8))# 绘制损失曲线plt.subplot(2, 2, 1)plt.plot(stats['total_loss'], 'b-', label='Total Loss')plt.plot(stats['pos_loss'], 'g--', label='Positive Loss')plt.plot(stats['neg_loss'], 'r--', label='Negative Loss')plt.xlabel('Epoch')plt.ylabel('Loss')plt.title('NCE Loss During Training')plt.legend()plt.grid(True)# 绘制准确率曲线plt.subplot(2, 2, 2)plt.plot(stats['accuracy'], 'g-')plt.xlabel('Epoch')plt.ylabel('Accuracy')plt.title('Binary Classification Accuracy')plt.grid(True)# 绘制每轮训练时间plt.subplot(2, 2, 3)plt.bar(range(1, len(stats['time_per_epoch']) + 1), stats['time_per_epoch'])plt.xlabel('Epoch')plt.ylabel('Time (seconds)')plt.title('Training Time per Epoch')plt.tight_layout()plt.show()def visualize_embeddings(model, corpus, top_n=50):"""使用t-SNE可视化词嵌入参数:model (ImprovedNCELoss): 训练好的模型corpus (torch.Tensor): 语料库top_n (int): 要可视化的高频词数量"""try:from sklearn.manifold import TSNEimport matplotlib.pyplot as plt# 获取词嵌入embeddings = model.center_embeddings.weight.detach().cpu().numpy()# 计算词频word_counts = {}for word_idx in corpus.cpu(): # 确保在CPU上处理word_idx = word_idx.item()word_counts[word_idx] = word_counts.get(word_idx, 0) + 1# 选取出现频率最高的词top_words = sorted(word_counts.items(), key=lambda x: x[1], reverse=True)[:top_n]top_word_indices = [word_idx for word_idx, _ in top_words]# 使用t-SNE降维tsne = TSNE(n_components=2, random_state=42, perplexity=min(30, len(top_word_indices) - 1))reduced_embeddings = tsne.fit_transform(embeddings[top_word_indices])# 绘制嵌入空间plt.figure(figsize=(10, 8))plt.scatter(reduced_embeddings[:, 0], reduced_embeddings[:, 1], s=50, c='steelblue', alpha=0.7)# 添加词索引标签for i, word_idx in enumerate(top_word_indices):plt.annotate(f"word_{word_idx}",(reduced_embeddings[i, 0], reduced_embeddings[i, 1]),fontsize=9)plt.title(f"t-SNE Visualization of Top {top_n} Word Embeddings")plt.xlabel("t-SNE Dimension 1")plt.ylabel("t-SNE Dimension 2")plt.tight_layout()plt.show()except ImportError:print("无法导入scikit-learn,跳过嵌入可视化")def similarity_analysis(model, word_idx1, word_idx2):"""分析两个词的相似度参数:model (ImprovedNCELoss): 训练好的模型word_idx1, word_idx2 (int): 要比较的词的索引"""# 获取词嵌入embed1 = model.center_embeddings.weight[word_idx1].detach().cpu()embed2 = model.center_embeddings.weight[word_idx2].detach().cpu()# 计算余弦相似度cos_sim = torch.nn.functional.cosine_similarity(embed1.unsqueeze(0), embed2.unsqueeze(0))print(f"词 {word_idx1} 和词 {word_idx2} 的余弦相似度: {cos_sim.item():.4f}")def find_similar_words(model, word_idx, top_k=5):"""找出与给定词最相似的top_k个词参数:model (ImprovedNCELoss): 训练好的模型word_idx (int): 目标词索引top_k (int): 返回的相似词数量"""# 获取所有词嵌入all_embeddings = model.center_embeddings.weight.detach().cpu()target_embedding = all_embeddings[word_idx]# 计算余弦相似度similarities = torch.nn.functional.cosine_similarity(target_embedding.unsqueeze(0), all_embeddings)# 获取最相似的词(排除自身)similarities[word_idx] = -1 # 排除自身top_similar = torch.topk(similarities, k=top_k)print(f"与词 {word_idx} 最相似的 {top_k} 个词:")for i, (idx, sim) in enumerate(zip(top_similar.indices, top_similar.values)):print(f" {i + 1}. 词 {idx.item()}: 相似度 {sim.item():.4f}")if __name__ == "__main__":# 设置随机种子以确保结果可复现torch.manual_seed(42)np.random.seed(42)try:# 训练模型model, stats = train_word_embeddings()# 示例:分析一些词的相似度print("\n词相似度分析示例:")similarity_analysis(model, 10, 20)# 示例:寻找相似词print("\n寻找相似词示例:")find_similar_words(model, 5, top_k=5)except Exception as e:print(f"出现错误: {e}")# 如果是CUDA相关错误,提供使用CPU的选项if "CUDA" in str(e):print("\n提示: 如果您的GPU内存不足或无法使用GPU,请修改代码改用CPU:")print("将 device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")")print("替换为 device = torch.device(\"cpu\")")
InfoNCE(SimCLR)实现
import torch
import torch.nn as nn
import torch.nn.functional as F
import torchvision
import torchvision.transforms as transforms
from torchvision.datasets import CIFAR10
from torch.utils.data import DataLoader
from torch.optim import Adam
from tqdm import tqdm
import matplotlib.pyplot as plt
from sklearn.manifold import TSNE# ------- SimCLR 数据增强 -------
class SimCLRTransform:def __init__(self, size=32):self.base_transform = transforms.Compose([transforms.RandomResizedCrop(size=size, scale=(0.2, 1.0)),transforms.RandomHorizontalFlip(),transforms.RandomApply([transforms.ColorJitter(0.4, 0.4, 0.4, 0.1)], p=0.8),transforms.RandomGrayscale(p=0.2),transforms.GaussianBlur(kernel_size=3),transforms.ToTensor(),transforms.Normalize(mean=[0.4914, 0.4822, 0.4465],std=[0.247, 0.243, 0.261])])def __call__(self, x):return self.base_transform(x), self.base_transform(x)# ------- 编码器 -------
class Encoder(nn.Module):def __init__(self):super().__init__()resnet = torchvision.models.resnet18(weights=None) # 替代 pretrained=Falseself.feature_extractor = nn.Sequential(*list(resnet.children())[:-1])self.feature_dim = resnet.fc.in_featuresdef forward(self, x):x = self.feature_extractor(x)return x.view(x.size(0), -1)# ------- 投影头 -------
class ProjectionHead(nn.Module):def __init__(self, in_dim=512, hidden_dim=2048, out_dim=128):super().__init__()self.projection = nn.Sequential(nn.Linear(in_dim, hidden_dim),nn.BatchNorm1d(hidden_dim),nn.ReLU(inplace=True),nn.Linear(hidden_dim, out_dim),nn.BatchNorm1d(out_dim))def forward(self, x):return self.projection(x)# ------- InfoNCE 损失 -------
def info_nce_loss(z1, z2, temperature=0.5):batch_size = z1.shape[0]z = torch.cat([z1, z2], dim=0)z = F.normalize(z, dim=1)sim_matrix = torch.matmul(z, z.T) / temperaturelabels = torch.arange(batch_size).to(z1.device)labels = torch.cat([labels, labels], dim=0)positive_mask = F.one_hot(labels, num_classes=2 * batch_size).float()positive_mask = torch.roll(positive_mask, shifts=batch_size, dims=1)logits_mask = 1 - torch.eye(2 * batch_size, device=z.device)exp_logits = torch.exp(sim_matrix) * logits_masklog_prob = sim_matrix - torch.log(exp_logits.sum(1, keepdim=True) + 1e-8)mean_log_prob_pos = (positive_mask * log_prob).sum(1) / positive_mask.sum(1)loss = -mean_log_prob_pos.mean()return loss# ------- SimCLR 训练 -------
def train_simclr(batch_size=256, epochs=100, temperature=0.5, device='cuda'):transform = SimCLRTransform()train_dataset = CIFAR10(root='./data', train=True, transform=transform, download=True)train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True, num_workers=4)encoder = Encoder().to(device)projection = ProjectionHead(encoder.feature_dim).to(device)optimizer = Adam(list(encoder.parameters()) + list(projection.parameters()), lr=1e-3)losses = []for epoch in range(epochs):encoder.train()projection.train()epoch_loss = 0for (x1, x2), _ in tqdm(train_loader, desc=f"Epoch {epoch+1}/{epochs}"):x1, x2 = x1.to(device), x2.to(device)h1, h2 = encoder(x1), encoder(x2)z1, z2 = projection(h1), projection(h2)loss = info_nce_loss(z1, z2, temperature)optimizer.zero_grad()loss.backward()optimizer.step()epoch_loss += loss.item()avg_loss = epoch_loss / len(train_loader)losses.append(avg_loss)print(f"[Epoch {epoch+1}] Loss: {avg_loss:.4f}")# 保存损失曲线图plt.figure()plt.plot(losses)plt.xlabel("Epoch")plt.ylabel("InfoNCE Loss")plt.title("SimCLR Training Loss")plt.savefig("simclr_loss_curve.png")print("✅ Loss 曲线已保存为 simclr_loss_curve.png")return encoder# ------- Linear Probe 线性分类器 -------
def linear_probe(encoder, device='cuda'):encoder.eval()for param in encoder.parameters():param.requires_grad = False# 用简单 transform 获取特征test_transform = transforms.Compose([transforms.ToTensor(),transforms.Normalize(mean=[0.4914, 0.4822, 0.4465],std=[0.247, 0.243, 0.261])])train_dataset = CIFAR10(root='./data', train=True, transform=test_transform, download=True)test_dataset = CIFAR10(root='./data', train=False, transform=test_transform, download=True)train_loader = DataLoader(train_dataset, batch_size=256, shuffle=True)test_loader = DataLoader(test_dataset, batch_size=256, shuffle=False)classifier = nn.Linear(encoder.feature_dim, 10).to(device)optimizer = Adam(classifier.parameters(), lr=1e-3)criterion = nn.CrossEntropyLoss()# 训练线性分类器for epoch in range(10):classifier.train()total_loss = 0for x, y in train_loader:x, y = x.to(device), y.to(device)with torch.no_grad():feat = encoder(x)pred = classifier(feat)loss = criterion(pred, y)optimizer.zero_grad()loss.backward()optimizer.step()total_loss += loss.item()print(f"[Linear Probe Epoch {epoch+1}] Loss: {total_loss/len(train_loader):.4f}")# 测试集准确率classifier.eval()correct = 0total = 0with torch.no_grad():for x, y in test_loader:x, y = x.to(device), y.to(device)feat = encoder(x)pred = classifier(feat)correct += (pred.argmax(1) == y).sum().item()total += y.size(0)acc = 100 * correct / totalprint(f"✅ Linear Probe Accuracy: {acc:.2f}%")return acc# ------- 特征可视化 -------
def visualize_tsne(encoder, device='cuda'):encoder.eval()transform = transforms.Compose([transforms.ToTensor(),transforms.Normalize(mean=[0.4914, 0.4822, 0.4465],std=[0.247, 0.243, 0.261])])dataset = CIFAR10(root='./data', train=False, transform=transform, download=True)loader = DataLoader(dataset, batch_size=256, shuffle=False)features, labels = [], []with torch.no_grad():for x, y in tqdm(loader, desc="Extracting features for t-SNE"):x = x.to(device)f = encoder(x)features.append(f.cpu())labels.append(y)features = torch.cat(features, dim=0).numpy()labels = torch.cat(labels, dim=0).numpy()tsne = TSNE(n_components=2, perplexity=30, init='pca', random_state=0)embedding = tsne.fit_transform(features)plt.figure(figsize=(10, 8))scatter = plt.scatter(embedding[:, 0], embedding[:, 1], c=labels, cmap='tab10', alpha=0.6)plt.legend(*scatter.legend_elements(), title="Classes")plt.title("t-SNE of SimCLR Features")plt.savefig("simclr_tsne.png")print("✅ t-SNE 图已保存为 simclr_tsne.png")# ------- 主函数 -------
if __name__ == "__main__":device = 'cuda' if torch.cuda.is_available() else 'cpu'encoder = train_simclr(batch_size=256, epochs=50, temperature=0.5, device=device)linear_probe(encoder, device=device)visualize_tsne(encoder, device=device)
SimSiam案例
SimSiam概述
SimSiam架构的流程:从一个原始图像 x x x 生成两个增强视图 x 1 x_1 x1 和 x 2 x_2 x2,然后通过特定的网络结构处理这两个视图,最终学习到图像的有意义的表示。图中包含以下关键组件:
- 两个相同的编码器网络(encoder f f f):用蓝色矩形表示,处理 x 1 x_1 x1 和 x 2 x_2 x2。
- 预测器(predictor h h h):用橙色小矩形表示,作用于 x 1 x_1 x1 的输出。
- 停止梯度(stop-grad)操作:应用于 x 2 x_2 x2 的输出,阻止梯度反向传播。
- 相似性(similarity):通过箭头表示,目标是最大化预测器输出和停止梯度后的输出之间的相似性。
SimSiam架构的详细解释
1. 输入图像
- 图中显示了两个增强视图 x 1 x_1 x1 和 x 2 x_2 x2,它们是从同一个原始图像 x x x 通过随机变换(如裁剪、旋转、颜色变换等)生成的。
- 自监督学习的核心思想是利用这些增强视图之间的关系,让模型学习图像的内在表示。
2. 编码器网络(encoder f f f)
- 组成:
- 骨干网络(backbone):通常是一个卷积神经网络(如ResNet),用于提取图像的特征。
- 投影多层感知机(projection MLP):一个小型神经网络,将骨干网络的输出映射到一个低维嵌入空间。
- 功能:两个编码器 f f f 分别处理 x 1 x_1 x1 和 x 2 x_2 x2,生成它们的嵌入表示。这两个编码器是相同的,共享参数。
- 图示:用蓝色矩形表示,分别标注为 f f f。
3. 预测器(predictor h h h)
- 组成:一个小型的多层感知机(MLP)。
- 功能:作用于编码器 f f f 对 x 1 x_1 x1 的输出(即 f ( x 1 ) f(x_1) f(x1)),生成一个预测表示,试图预测 x 2 x_2 x2 的表示。
- 图示:用橙色小矩形表示,连接在 x 1 x_1 x1 的编码器输出之后。
4. 停止梯度(stop-grad)
- 功能:应用于编码器 f f f 对 x 2 x_2 x2 的输出(即 f ( x 2 ) f(x_2) f(x2)),阻止梯度通过这个路径反向传播。这意味着在训练时,编码器 f f f 的参数不会通过 x 2 x_2 x2 的路径更新。
- 图示:用一个类似“x”的符号和向下箭头表示,位于 x 2 x_2 x2 的编码器输出之后。
5. 相似性(similarity)
- 功能:图中有一个标有“similarity”的箭头,连接预测器 h h h 的输出(基于 x 1 x_1 x1)和停止梯度后的 f ( x 2 ) f(x_2) f(x2)。模型的目标是最大化这两者之间的相似性。
- 实现:通常使用余弦相似度作为损失函数,鼓励两个表示向量之间的夹角变小。
SimSiam的工作原理
SimSiam通过以下步骤学习图像的表示:
- 从原始图像 x x x 生成两个增强视图 x 1 x_1 x1 和 x 2 x_2 x2。
- 将 x 1 x_1 x1 和 x 2 x_2 x2 输入两个相同的编码器 f f f,分别得到嵌入表示 f ( x 1 ) f(x_1) f(x1) 和 f ( x 2 ) f(x_2) f(x2)。
- 对 f ( x 1 ) f(x_1) f(x1) 应用预测器 h h h,生成预测表示 h ( f ( x 1 ) ) h(f(x_1)) h(f(x1))。
- 对 f ( x 2 ) f(x_2) f(x2) 应用停止梯度操作,得到 stop-grad ( f ( x 2 ) ) \text{stop-grad}(f(x_2)) stop-grad(f(x2))。
- 模型通过最小化 h ( f ( x 1 ) ) h(f(x_1)) h(f(x1)) 和 stop-grad ( f ( x 2 ) ) \text{stop-grad}(f(x_2)) stop-grad(f(x2)) 之间的差异(即最大化它们的相似性),学习对图像内容不变的表示。
stop-grad
停止梯度操作(stop-gradient operation)是一种在神经网络训练中使用的技术,用来阻止梯度在反向传播时流向某些特定的路径。在深度学习框架中,这通常通过特定的函数实现,比如在 PyTorch 中使用 detach()
,在 TensorFlow 中使用 tf.stop_gradient()
。
1. 前向传播:stop-grad(f(x₂))=f(x₂)
- 在前向传播(也就是从输入到输出的计算过程)中,
stop-grad(f(x₂))
的值就是f(x₂)
。这里的f
通常是一个编码器(比如神经网络),x₂
是输入数据,f(x₂)
是编码器对x₂
的输出。 stop-grad
只是一个操作,它不会改变f(x₂)
的数值结果。所以,如果你在损失函数中用到了stop-grad(f(x₂))
,比如计算h(f(x₁))
和stop-grad(f(x₂))
之间的差异,实际上就是在计算h(f(x₁))
和f(x₂)
的差异。- 简单来说,前向传播时,
stop-grad
“看不见”它的作用,它就像一个透明的传递层。
2. 反向传播:stop-grad 的关键作用
- 在反向传播(也就是计算梯度并更新参数的过程)中,
stop-grad
的作用就显现出来了:它会阻止梯度通过f(x₂)
回传到编码器f
。 - 具体来说,当你计算损失函数关于模型参数的梯度时,
stop-grad(f(x₂))
被视为一个常量。常量在求导时梯度为零,所以梯度不会流回f
,也不会影响f
的参数更新。 - 举个例子:
- 假设损失函数是 L = ∣ ∣ h ( f ( x 1 ) ) − s t o p − g r a d ( f ( x 2 ) ) ∣ ∣ 2 L=||h(f(x₁))-stop-grad(f(x₂))||² L=∣∣h(f(x1))−stop−grad(f(x2))∣∣2。
- 前向传播时,计算的是 h ( f ( x 1 ) ) h(f(x₁)) h(f(x1)) 和 f ( x 2 ) f(x₂) f(x2) 的差。
- 反向传播时,梯度会流向 h h h 和 f ( x 1 ) f(x₁) f(x1),但因为
stop-grad
,梯度不会流向 f ( x 2 ) f(x₂) f(x2) 或编码器 f f f 在处理 x 2 x₂ x2 时的参数。
3. 为什么会有这种区别?
- 你可能会问:既然前向传播时
stop-grad(f(x₂))
就是f(x₂)
,为什么不在损失函数里直接用 f ( x 2 ) f(x₂) f(x2)? - 答案在于模型训练的目标。如果直接用 f ( x 2 ) f(x₂) f(x2),梯度会同时流向 f ( x 1 ) f(x₁) f(x1) 和 f ( x 2 ) f(x₂) f(x2),模型可能会“作弊”:让 f ( x 1 ) f(x₁) f(x1) 和 f ( x 2 ) f(x₂) f(x2) 输出相同的值(比如全零向量),这样损失会很小,但这种表示没有意义,因为所有输入都被映射成了同一个点。
- 使用
stop-grad(f(x₂))
,梯度只流向 f ( x 1 ) f(x₁) f(x1) 和 h h h,迫使模型调整 h ( f ( x 1 ) ) h(f(x₁)) h(f(x1)) 去匹配 f ( x 2 ) f(x₂) f(x2),而 f ( x 2 ) f(x₂) f(x2) 保持不变(因为没有梯度更新)。这能让编码器 f f f 学到更有意义的表示,而不是简单的坍塌解。
- s t o p − g r a d ( f ( x 2 ) ) stop-grad(f(x₂)) stop−grad(f(x2)) 在前向传播时就是 f ( x 2 ) f(x₂) f(x2),数值上完全一样。
- 但在反向传播时,
stop-grad
阻止了梯度回传到 f ( x 2 ) f(x₂) f(x2),这使得模型的参数更新只依赖于 h ( f ( x 1 ) ) h(f(x₁)) h(f(x1)) 的调整,而不是直接改变 f ( x 2 ) f(x₂) f(x2)。 - 这种设计是为了避免模型学到退化的解,确保编码器 f f f 能提取出对输入数据有意义的特征。
SimSiam代码实现
import torch
import torch.nn as nn
import torch.nn.functional as F
from torchvision import datasets, transforms, models
from torch.utils.data import DataLoader
from torch.optim import SGD
import matplotlib.pyplot as plt
import os
from tqdm import tqdm# -------------------------------
# 1. 数据增强模块(两个视图)
# -------------------------------
class SimSiamTransform:"""SimSiam 的数据增强模块,生成两个视图(类似 SimCLR)"""def __init__(self, image_size=32):self.transform = transforms.Compose([transforms.RandomResizedCrop(image_size, scale=(0.2, 1.0)),transforms.RandomHorizontalFlip(),transforms.ColorJitter(0.4, 0.4, 0.4, 0.1),transforms.RandomGrayscale(p=0.2),transforms.ToTensor(),transforms.Normalize(mean=[0.4914, 0.4822, 0.4465], std=[0.247, 0.243, 0.261])])def __call__(self, x):return self.transform(x), self.transform(x)# -------------------------------
# 2. Encoder + Projection + Prediction Head
# -------------------------------
class MLPHead(nn.Module):"""投影/预测头,Linear -> BN -> ReLU -> Linear"""def __init__(self, in_dim, hidden_dim=2048, out_dim=2048):super().__init__()self.net = nn.Sequential(nn.Linear(in_dim, hidden_dim),nn.BatchNorm1d(hidden_dim),nn.ReLU(inplace=True),nn.Linear(hidden_dim, out_dim))def forward(self, x):return self.net(x)class SimSiam(nn.Module):"""SimSiam 主网络:encoder -> projection -> prediction"""def __init__(self, backbone_dim=512):super().__init__()# 预训练 backbone(去掉最后全连接层)backbone = models.resnet18(weights=None) # 不加载预训练权重self.encoder = nn.Sequential(*list(backbone.children())[:-1]) # 去除分类层# 投影头 g()self.projector = MLPHead(backbone_dim, 2048, 2048)# 预测头 h()self.predictor = MLPHead(2048, 512, 2048)def forward(self, x1, x2):# 提取两个视图的特征f1 = self.encoder(x1).flatten(start_dim=1) # [B, 512]f2 = self.encoder(x2).flatten(start_dim=1)z1 = self.projector(f1) # g(f(x1))z2 = self.projector(f2)p1 = self.predictor(z1) # h(g(f(x1)))p2 = self.predictor(z2)return p1, p2, z1.detach(), z2.detach() # stop-grad 作用于 z1/z2# -------------------------------
# 3. SimSiam 损失函数(我们希望两个特征向量(预测结果和目标结果)越接近越好,也就是它们之间的余弦相似度越高,最好能接近 1。由于我们使用的优化器都是“最小化”损失的,所以我们把“相似度越高(越好)”转变成“损失越低”。把余弦相似度取负:当两个向量很相似时,余弦相似度接近 1,负值就是 -1;而如果不相似时,负值会更高(例如 0 或正值)因此,最小化这个负值就能让模型学到让两个向量更相似的表示。)
# -------------------------------
def D(p, z):"""负向 cosine similarity"""p = F.normalize(p, dim=1)z = F.normalize(z, dim=1)return - (p * z).sum(dim=1).mean()def simsiam_loss(p1, p2, z1, z2):return D(p1, z2) / 2 + D(p2, z1) / 2# -------------------------------
# 4. 训练过程
# -------------------------------
def train_simsiam(batch_size=512, epochs=50, lr=0.05):device = torch.device("cuda" if torch.cuda.is_available() else "cpu")transform = SimSiamTransform()dataset = datasets.CIFAR10(root="./data", train=True, transform=transform, download=True)loader = DataLoader(dataset, batch_size=batch_size, shuffle=True, num_workers=4, drop_last=True)model = SimSiam().to(device)optimizer = SGD(model.parameters(), lr=lr, momentum=0.9, weight_decay=1e-4)loss_list = []for epoch in range(1, epochs + 1):model.train()total_loss = 0for (x1, x2), _ in tqdm(loader, desc=f"Epoch {epoch}/{epochs}"):x1, x2 = x1.to(device), x2.to(device)p1, p2, z1, z2 = model(x1, x2)loss = simsiam_loss(p1, p2, z1, z2)optimizer.zero_grad()loss.backward()optimizer.step()total_loss += loss.item()avg_loss = total_loss / len(loader)loss_list.append(avg_loss)print(f"[Epoch {epoch}] Loss: {avg_loss:.4f}")# 保存训练损失图os.makedirs("output", exist_ok=True)plt.plot(loss_list)plt.title("SimSiam Training Loss")plt.xlabel("Epoch")plt.ylabel("Loss")plt.savefig("output/simsiam_loss_curve.png")plt.close()print("训练完成,损失图已保存到 output/simsiam_loss_curve.png")return modelif __name__ == "__main__":train_simsiam()
相关文章:
对比学习中的NCE(Noise-Contrastive Estimation)和InfoNCE(SimCLR)损失函数+案例(附SimSiam分析)
在对比学习(Contrastive Learning)中,NCE(Noise-Contrastive Estimation)和InfoNCE是两种常见的目标函数,它们都用于通过区分正样本和负样本来学习高质量的表示。 1. NCE(Noise-Contrastive Est…...
基于FAN网络的图像识别系统设计与实现
基于FAN网络的图像识别系统设计与实现 一、系统概述 本系统旨在利用FAN(Fourier Analysis Networks)网络架构实现高效的图像识别功能,并通过Python语言设计一个直观的用户界面,方便用户操作与使用。FAN网络在处理周期性特征方面具有独特优势,有望提升图像识别在复杂场景…...
【瑞萨 RA-Eco-RA2E1-48PIN-V1.0 开发板测评】PWM
【瑞萨 RA-Eco-RA2E1-48PIN-V1.0 开发板测评】PWM 本文介绍了瑞萨 RA2E1 开发板使用内置时钟和定时器实现 PWM 输出以及呼吸灯的项目设计。 项目介绍 介绍了 PWM 和 RA2E1 的 PWM 资源。 PWM 脉冲宽度调制(Pulse Width Modulation, PWM)是一种对模拟…...
NDK开发:开发环境
NDK开发环境 一、NDK简介 1.1 什么是NDK NDK(Native Development Kit)是Android提供的一套工具集,允许开发者在Android应用中使用C/C++代码。它包含了: 交叉编译器构建工具调试器系统头文件和库示例代码和文档1.2 NDK的优势 性能优化:直接使用底层代码,提高性能代码保…...
设计模式简述(三)工厂模式
工厂模式 描述简单工厂(静态工厂)工厂方法模式 抽象工厂增加工厂管理类使用 描述 工厂模式用以封装复杂的实例初始化过程,供外部统一调用 简单工厂(静态工厂) 如果对象创建逻辑简单且一致,可以使用简单工…...
通过Postman和OAuth 2.0连接Dynamics 365 Online的详细步骤
🌟 引言 在企业应用开发中,Dynamics 365 Online作为微软的核心CRM平台,提供了强大的Web API接口。本文将教你如何通过Postman和OAuth 2.0认证实现与Dynamics 365的安全连接,轻松调用数据接口。 📝 准备工作 工具安装…...
LlamaIndex实现RAG增强:上下文增强检索/重排序
面向文档检索的上下文增强技术 文章目录 面向文档检索的上下文增强技术概述技术背景核心组件方法详解文档预处理向量存储创建上下文增强检索检索对比技术优势结论导入库和环境变量读取文档创建向量存储和检索器数据摄取管道使用句子分割器的摄取管道使用句子窗口的摄取管道查询…...
AI比人脑更强,因为被植入思维模型【43】蝴蝶效应思维模型
giszz的理解:蝴蝶效应我们都熟知,就是说一个微小的变化,能带动整个系统甚至系统的空间和时间的远端,产生巨大的链式反应。我学习后的启迪,简单的说,就是不要忽视任何微小的问题,更多时候&#x…...
程序化广告行业(62/89):DSP系统的媒体与PDB投放设置探秘
程序化广告行业(62/89):DSP系统的媒体与PDB投放设置探秘 大家好!在之前的学习中,我们对程序化广告的DSP系统有了一定了解。今天还是带着和大家共同进步的想法,深入探索DSP系统中媒体设置以及PDB投放设置的…...
Java项目之基于ssm的怀旧唱片售卖系统(源码+文档)
项目简介 怀旧唱片售卖系统实现了以下功能: 用户信息管理: 用户信息新增:添加新用户的信息。 用户信息修改:对现有用户信息进行修改。 商品信息管理: 商品信息添加:增加新的商品(唱片&#x…...
程序化广告行业(61/89):DSP系统活动设置深度剖析
程序化广告行业(61/89):DSP系统活动设置深度剖析 大家好!在程序化广告的学习道路上,我们已经探索了不少重要内容。今天依旧本着和大家一起学习进步的想法,深入解析DSP系统中活动设置的相关知识。这部分内容…...
Altshuller矛盾矩阵查询:基于python和streamlit
基于python和streamlit实现的Altshuller矛盾矩阵查询 import streamlit as st import json# 加载数据 st.cache_resource def load_data():with open(parameter.json, encodingutf-8) as f:parameters json.load(f)with open(way.json, encodingutf-8) as f:contradictions …...
FreeRTOS的空闲任务
在 FreeRTOS 中,空闲任务(Idle Task) 是操作系统自动创建的一个特殊任务,其作用和管理方式如下: 1. 空闲任务创建 FreeRTOS 内核自动创建:当调用 vTaskStartScheduler() 启动调度器时,内核会自…...
【代码模板】如何用FILE操作符打开文件?fopen、fclose
#include "stdio.h" #include "unistd.h"int main(int argc, char *argv[]) {FILE *fp fopen("1.log", "wb");if (!fp) {perror("Failed open 1.log");return -1;}fclose(fp); }关于权限部分参考兄弟篇【代码模板】C语言中…...
[特殊字符] Pandas 常用操作对比:Python 运算符 vs Pandas 函数
在 Pandas 中,许多操作可以直接使用 Python 的比较运算符(如 、!、>、< 等),而不需要调用 Pandas 的专门函数(如 eq()、ne()、gt() 等)。这些运算符在 Pandas 中已经被重载,代码更简洁。以…...
I.MX6ULL开发板与linux互传文件的方法--NFS,SCP,mount
1、内存卡或者U盘 方法比较简单,首先在linux系统中找到u盘对应的文件夹,随后使用cp指令将文件拷贝进u盘。 随后将u盘插入开发板中,找到u盘对应的设备文件。一般u盘对应的设备文件在/dev下,以sda开头,可以使用命令列出所…...
图解AUTOSAR_SWS_FlashEEPROMEmulation
AUTOSAR Flash EEPROM Emulation (FEE) 详解 基于AUTOSAR规范的Flash EEPROM Emulation模块分析 目录 1. 概述2. 架构设计 2.1 模块位置与接口2.2 内部状态管理2.3 配置结构3. API接口 3.1 接口功能分类3.2 错误管理4. 操作流程 4.1 写入操作序列5. 总结1. 概述 Flash EEPROM …...
Unity:Simple Follow Camera(简单相机跟随)
为什么需要Simple Follow Camera? 在游戏开发中,相机(Camera)是玩家的“眼睛”。它的作用是决定玩家看到游戏世界的哪一部分。很多游戏需要相机自动跟随玩家角色,让玩家始终可以看到角色及其周围的环境,而…...
[项目总结] 在线OJ刷题系统项目总结与分析(二): 技术应用(上)
🌸个人主页:https://blog.csdn.net/2301_80050796?spm1000.2115.3001.5343 🏵️热门专栏: 🧊 Java基本语法(97平均质量分)https://blog.csdn.net/2301_80050796/category_12615970.html?spm1001.2014.3001.5482 🍕 Collection与…...
针对Ansible执行脚本时报错“可执行文件格式错误”,以下是详细的解决步骤和示例
针对Ansible执行脚本时报错“可执行文件格式错误”,以下是详细的解决步骤和示例: 目录 一、错误原因分析二、解决方案1. 检查并添加可执行权限2. 修复Shebang行3. 转换文件格式(Windows → Unix)4. 检查脚本内容兼容性5. 显式指定…...
从 Dense LLM 到 MoE LLM:以 DeepSeek MoE 为例讲解 MoE 的基本原理
写在前面 大多数 LLM 均采用 Dense(密集) 架构。这意味着,在处理每一个输入 Token 时,模型所有的参数都会被激活和计算。想象一下,为了回答一个简单的问题,你需要阅读整部大英百科全书的每一个字——这显然效率低下。 为了突破 Dense 模型的瓶颈,一种名为 Mixture of …...
未来已来:探索AI驱动的HMI设计新方向
在科技浪潮的持续冲击下,人工智能(AI)正以势不可挡的姿态重塑各个领域的格局,其中人机交互(HMI,Human - Machine Interaction)设计领域深受其影响,正经历着深刻的变革。AI 技术的融入…...
5天速成ai agent智能体camel-ai之第1天:camel-ai安装和智能体交流消息讲解(附源码,零基础可学习运行)
嗨,朋友们!👋 是不是感觉AI浪潮铺天盖地,身边的人都在谈论AI Agent、大模型,而你看着那些密密麻麻的代码,感觉像在读天书?🤯 别焦虑!你不是一个人。很多人都想抓住AI的风…...
Unity UGUI使用手册
概述 UGUI(Unity Graphical User Interface) :Unity 图像用户界面 在游戏开发中,我们经常需要搭建一些图形用户界面。Unity内置的UGUI可以帮助开发者可视化地拼接界面,提高开发效率。UGUI提供不同样式的UI组件,并且封装了对应功能的API&am…...
(二)输入输出处理——打造智能对话的灵魂
上一篇:(一)从零开始:用 LangChain 和 ZhipuAI 搭建简单对话 在上一篇文章中,我们成功搭建了一个基于 LangChain 和 ZhipuAI 的智能对话系统的基础环境。今天,我们将深入探讨输入输出处理的细节࿰…...
beego文件上传
1file.go 2html代码 3路由设置 beego.Router("/file/Upload", &controllers.FileUploadController{}, "post:Upload") 注意 1,得新建个upload文件夹 2,路由设置严格区分大小写。 biiego文件下载上传代码 github 觉得不错Star下...
代码随想录回溯算法01(递归)
回溯法也可以叫做回溯搜索法,它是一种搜索的方式。 回溯是递归的副产品,只要有递归就会有回溯。 所以以下讲解中,回溯函数也就是递归函数,指的都是一个函数。 组合问题:N个数里面按一定规则找出k个数的集合切割问题&am…...
分治-归并排序-逆序对问题
目录 1.升序(以右边的合并组为基准) 2.降序(以左边的合并组为基准) 3.逆对序--固定下标 1.升序(以右边的合并组为基准) 找出左边有多少个数比我(nums[right])大 应该在每一次合并之前,进行…...
mysql-getshell的几种方法
mysql_getshell的几种方法 mysql_getshell 一、mysql的–os-shell 利用原理 –os-shell就是使用udf提权获取WebShell。也是通过into oufile向服务器写入两个文件,一个可以直接执行系统命令,一个进行上传文件。此为sqlmap的一个命令,利用这…...
初阶数据结构--树
1. 树的概念与结构 树是⼀种⾮线性的数据结构,它是由 n(n>0) 个有限结点组成⼀个具有层次关系的集合。把它叫做 树是因为它看起来像⼀棵倒挂的树,也就是说它是根朝上,⽽叶朝下的。 有⼀个特殊的结点,称…...
搭建redis主从同步实现读写分离(原理剖析)
搭建redis主从同步实现读写分离(原理剖析) 文章目录 搭建redis主从同步实现读写分离(原理剖析)前言一、搭建主从同步二、同步原理 前言 为什么要学习redis主从同步,实现读写分析。因为单机的redis虽然是基于内存,单机并发已经能支撑很高。但是随着业务量…...
Python3 学习笔记
Python3 简介 | 菜鸟教程 一 Python3 简介 Python 是一个高层次的结合了解释性、编译性、互动性和面向对象的脚本语言。 Python 的设计具有很强的可读性,相比其他语言经常使用英文关键字,其他语言的一些标点符号,它具有比其他语言更有特色…...
kmpmanacher
KMP 理论 KMP算法的核心是构建一个部分匹配表,也称为前缀表。这个表记录了模式串中每个位置之前的最长公共前缀和后缀的长度。例如,对于模式串"ababaca",其部分匹配表如下: 位置0123456字符ababaca最长公共前后缀长度…...
ts基础知识总结
TypeScript(简称TS)是JavaScript(简称JS)的一个超集,它在JS的基础上增加了静态类型检查、类、模块等特性。 TypeScript 与 JavaScript 的不同及好处 不同点 类型系统 JavaScript 是一种弱类型语言,这意味…...
操作系统内存管理
为什么要有虚拟内存 单片机的CPU直接操作内存的物理地址,这就导致在内存中同时运行两个程序是不可能的,有可能会出现第一个程序在2000的位置写入新的值将会擦掉第二个程序存放在相同位置上的内容。 出现这个问题的根本原因是两个程序引用了绝对物理地址。…...
M芯片,能运行普通应用程序的原架构虚拟机
在我们使用搭载了Apple芯片的Mac时,很多时候会用到windows虚拟机来使用windows应用程序 但是Apple芯片是ARM架构,如果运行原价构的虚拟机,很多64位的普通应用程序就无法运行,如果使用UTM来安装64位的跨架构虚拟机,就会非常卡慢 但实际上使用一种特殊的系统镜像,就可以使用ARM…...
多功能指示牌的主要功能有哪些?
哇哦!咱们的多功能指示牌可有着超多超厉害的主要功能哦,简直就是生活中的超级小助手,涵盖了方方面面呢! 指示导向功能 道路指引:不管是在繁华热闹的城市道路,还是车水马龙的高速公路,亦或是风…...
Superset 问题
和nginx结合使用,如果不是配置到根路径,会比较麻烦,我试了很多种方法,也就 这个 靠谱点,不过,我最后还是选择的部署在根路径,先探索一番再说默认不能选择mysql数据库,需要安装mysql客…...
安装gpu版本的dgl
1.先去网址,找到对应版本的dgl,然后下载到本地。 dgl-whl下载地址 我的是python 3.8 ,cuda 11.6. windows 2.在虚拟环境里 输入 pip install E:\dgl-1.0.2cu116-cp38-cp38-win_amd64.whl (因为我下载到E盘里了) 这样GPU版本的d…...
vue watch和 watchEffect
在 Vue 3 中,watch 和 watchEffect 是两个用于响应式地监听数据变化并执行副作用的 API。它们在功能上有一些相似之处,但用途和行为有所不同。以下是对 watch 和 watchEffect 的详细对比和解释: 1. watch watch 是一个更通用的 API…...
JavaScript基础--03-变量的数据类型:基本数据类型和引用数据类型
JavaScript基础--03-变量的数据类型:基本数据类型和引用数据类型 前言变量的数据类型为什么需要数据类型JS中一共有六种数据类型 一个经典的例子栈内存和堆内存 前言 我们接着上一篇文章 JavaScript基础–02-变量 来讲。 下一篇文章 JavaScript基础–04-基本数据类…...
WindowsPE文件格式入门05.PE加载器LoadPE
https://bpsend.net/thread-316-1-1.html LoadPE - pe 加载器 壳的前身 如果想访问一个程序运行起来的内存,一种方法就是跨进程读写内存,但是跨进程读写内存需要来回调用api,不如直接访问地址来得方便,那么如果我们需要直接访问地址,该怎么做呢?.需要把dll注进程,注进去的代码…...
【Redis】通用命令
使用者通过redis-cli客户端和redis服务器交互,涉及到很多的redis命令,redis的命令非常多,我们需要多练习常用的命令,以及学会使用redis的文档。 一、get和set命令(最核心的命令) Redis中最核心的两个命令&…...
Android学习总结之service篇
引言 在 Android 开发里,Service 与 IntentService 是非常关键的组件,它们能够让应用在后台开展长时间运行的操作。不过,很多开发者仅仅停留在使用这两个组件的层面,对其内部的源码实现了解甚少。本文将深入剖析 Service 和 Inte…...
基于CATIA产品结构树智能排序的二次开发技术解析——深度定制BOM层级管理系统的Pycatia实践
引言 在航空制造与汽车装配领域,CATIA产品结构树(Product Tree)的规范性直接影响MBOM管理效率。传统手动排序存在两大痛点: 多级编号混乱:混合零件号(PartNumber)与实例名(Insta…...
机器人轨迹跟踪控制——CLF-CBF-QP
本次使用MATLAB复现CLF-CBF-QP算法,以实现机器人轨迹跟踪同时保证安全性能 模型 使用自行车模型来进行模拟机器人的移动动态,具体的模型推导参考车辆运动学模型-自行车模型 采用偏差变量 p ~ = p − p r e f u ~ = u − u r e f \tilde{p} = p - p_{ref} \\ \tilde{u} = …...
道路裂缝数据集CrackForest-156-labelme
来源于开源的数据集 https://github.com/cuilimeng/CrackForest-dataset 进行整理修改而成。 文章目录 1. 介绍2. 数据文件3. 应用场景4. 相关工具5. 下载地址 1. 介绍 在现代城市管理中,道路状况的监测与维护是确保交通安全和城市基础设施健康的重要环节。 CrackF…...
数据定义语言
一、DDL的核心功能 DDL用于定义和管理数据库对象的结构,包括数据库、表、索引、视图等,主要操作包括创建、修改、删除。其核心命令包括: CREATE:创建对象(数据库、表、索引等) ALTER:修改对象结构(如添加/删除列) DROP:删除对象 TRUNCATE:清空表数据(保留结构) RE…...
爬楼梯问题-动态规划
一、题目 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢? 示例 1: 输入:n 2 输出:2 解释:有两种方法可以爬到楼顶。 方法1. 1 阶 1 阶 方法2. 2 阶…...
MySQL篇(四)事务相关知识详解
MySQL篇(四)事务相关知识详解 MySQL篇(四)事务相关知识详解一、事务的特性(ACID)原子性(Atomicity)一致性(Consistency)隔离性(Isolation)持久性(…...