一、Snorkel 库核心概述
1.1 用途与工作原理
Snorkel 是一款专为弱监督学习(Weakly Supervised Learning)设计的 Python 库,核心解决机器学习中标注数据稀缺、标注成本高昂的痛点。它允许开发者通过编写简单的标注函数(Labeling Functions, LFs)、集成多个弱监督信号,无需人工标注大量数据,就能快速生成高质量的训练标签,进而训练出性能优异的机器学习模型。

其工作原理可概括为三步:首先,用户针对任务编写多个标注函数,每个函数基于不同的启发式规则、外部知识库或弱监督信号对数据进行标注;其次,Snorkel 的标签模型(Label Model)会自动学习这些标注函数的可靠性权重,解决函数间的冲突与冗余,输出概率化的训练标签;最后,用生成的标签训练下游任务模型(如分类器),完成端到端的弱监督学习流程。
1.2 优缺点分析
优点
- 大幅降低标注成本:无需人工标注数千上万条数据,仅需编写少量标注函数即可生成训练标签,效率提升显著。
- 灵活性强:支持文本分类、实体识别、图像分类等多种任务,标注函数可灵活结合规则、正则表达式、外部模型等多种信号。
- 标签质量可控:标签模型通过学习标注函数的可靠性,有效过滤噪声标签,生成的标签质量优于单一规则标注。
- 与主流框架兼容:可无缝对接 Scikit-learn、TensorFlow、PyTorch 等主流机器学习/深度学习框架,适配现有工作流。
缺点
- 有一定学习门槛:需要用户理解弱监督学习的核心思想,掌握标注函数的编写逻辑,对新手不够友好。
- 标注函数编写依赖领域知识:针对特定任务的标注函数需要结合领域经验,否则可能导致标签质量下降。
- 性能受限于标注函数质量:若标注函数设计不合理、覆盖场景不全,最终模型性能会大打折扣。
1.3 License 类型
Snorkel 采用 Apache License 2.0 开源协议,该协议允许用户自由使用、修改、分发源代码,可用于商业项目,仅需保留原作者版权声明和协议文本。
二、Snorkel 安装与环境配置
2.1 安装方式
Snorkel 支持多种安装方式,推荐使用 pip 进行快速安装,同时需确保 Python 版本在 3.7~3.10 之间(版本过高可能存在兼容性问题)。
方法1:PyPI 官方安装
打开命令行终端,执行以下命令:
pip install snorkel方法2:源码编译安装
若需要使用最新开发版本,可从 GitHub 克隆源码并安装:
# 克隆仓库
git clone https://github.com/snorkel-team/snorkel.git
# 进入仓库目录
cd snorkel
# 安装依赖并编译
pip install -r requirements.txt
pip install -e .2.2 环境验证
安装完成后,可通过以下 Python 代码验证是否安装成功:
import snorkel
# 打印 Snorkel 版本号
print(f"Snorkel 版本:{snorkel.__version__}")
# 验证核心模块是否可用
from snorkel.labeling import LabelingFunction, LFApplier
print("核心模块导入成功!")若运行无报错且输出版本号,则说明环境配置完成。
三、Snorkel 核心概念与基础用法
3.1 核心概念解析
在使用 Snorkel 前,需先理解以下几个核心概念,这是构建弱监督学习流程的基础:
- 标注函数(Labeling Function, LF):弱监督学习的核心,是用户编写的、基于启发式规则对数据进行标注的函数。每个 LF 可以对数据样本标注正类(1)、负类(0)、弃权(-1)三种标签之一,弃权表示该函数无法判断该样本的类别。
- 标签模型(Label Model):Snorkel 的核心组件,用于自动学习多个 LF 的可靠性权重,解决 LF 之间的冲突(如一个 LF 标正类,另一个标负类)和冗余,最终输出每个样本的概率化标签。
- 标签应用器(LFApplier):用于将所有标注函数应用到数据集上,生成一个标签矩阵(Label Matrix),矩阵的每一行对应一个样本,每一列对应一个 LF 的标注结果。
- 下游任务模型(Downstream Model):使用标签模型生成的标签进行训练的模型,如文本分类器、实体识别器等,可根据任务需求选择传统机器学习模型或深度学习模型。
3.2 基础工作流程
Snorkel 的典型工作流程分为四步:编写标注函数 → 生成标签矩阵 → 训练标签模型 → 训练下游模型。下面以文本情感分类任务为例,详细演示每一步的实现方法。
四、实战:基于 Snorkel 的文本情感分类
本次实战任务为电影评论情感分类,目标是将评论分为正面(1)和负面(0)两类。我们将使用 Snorkel 编写标注函数,无需人工标注数据,直接生成训练标签并训练分类器。
4.1 数据集准备
我们使用 Snorkel 内置的小型电影评论数据集,也可替换为自定义数据集。首先导入所需模块并加载数据:
import pandas as pd
from snorkel.datasets import load_movie_reviews
# 加载数据集
train_df, test_df = load_movie_reviews()
# 查看数据集结构
print("训练集样本数:", len(train_df))
print("测试集样本数:", len(test_df))
# 查看前5条训练数据
print(train_df[["text", "sentiment"]].head())数据集的 text 列是电影评论文本,sentiment 列是真实情感标签(1 为正面,0 为负面),在弱监督学习中,我们不会使用真实标签,仅用于最终测试模型性能。
4.2 编写标注函数
标注函数是弱监督学习的核心,我们需要结合情感分类的任务特点,编写多个基于关键词、正则表达式的 LF。首先定义标签常量:
# 定义标签常量
ABSTAIN = -1
POSITIVE = 1
NEGATIVE = 0接下来编写 5 个不同的标注函数,分别基于正面关键词、负面关键词、情感强度词、否定词、长度规则进行标注:
标注函数1:基于正面关键词标注
该函数判断评论中是否包含正面关键词(如 “great”、”excellent”、”amazing”),若包含则标注为正面(1),否则弃权(-1)。
from snorkel.labeling import LabelingFunction
# 定义正面关键词列表
positive_keywords = ["great", "excellent", "amazing", "fantastic", "wonderful", "perfect"]
@LabelingFunction()
def lf_positive_keywords(x):
"""基于正面关键词的标注函数"""
return POSITIVE if any(word in x.text.lower() for word in positive_keywords) else ABSTAIN标注函数2:基于负面关键词标注
该函数判断评论中是否包含负面关键词(如 “bad”、”terrible”、”awful”),若包含则标注为负面(0),否则弃权(-1)。
# 定义负面关键词列表
negative_keywords = ["bad", "terrible", "awful", "horrible", "disappointing", "worst"]
@LabelingFunction()
def lf_negative_keywords(x):
"""基于负面关键词的标注函数"""
return NEGATIVE if any(word in x.text.lower() for word in negative_keywords) else ABSTAIN标注函数3:基于情感强度词标注
该函数判断评论中是否包含强情感词(如 “love”、”hate”),”love” 对应正面,”hate” 对应负面。
@LabelingFunction()
def lf_sentiment_intensity(x):
"""基于情感强度词的标注函数"""
text = x.text.lower()
if "love" in text:
return POSITIVE
elif "hate" in text:
return NEGATIVE
else:
return ABSTAIN标注函数4:基于否定词的标注函数
该函数处理包含否定词的情况,如 “not great” 应标注为负面,”not bad” 应标注为正面。
import re
@LabelingFunction()
def lf_negative_expressions(x):
"""基于否定词的标注函数"""
text = x.text.lower()
# 匹配 "not + 正面词" 结构
if re.search(r"not (great|excellent|amazing|good)", text):
return NEGATIVE
# 匹配 "not + 负面词" 结构
elif re.search(r"not (bad|terrible|awful)", text):
return POSITIVE
else:
return ABSTAIN标注函数5:基于评论长度的标注函数
通常,正面评论可能更长(用户愿意详细分享体验),负面评论可能更短。该函数设定评论长度阈值,长评论标注为正面,短评论标注为负面。
@LabelingFunction()
def lf_review_length(x):
"""基于评论长度的标注函数"""
# 计算单词数量
word_count = len(x.text.split())
if word_count > 50:
return POSITIVE
elif word_count < 10:
return NEGATIVE
else:
return ABSTAIN4.3 生成标签矩阵
编写完标注函数后,需要使用 LFApplier 将这些函数应用到训练数据集上,生成标签矩阵。标签矩阵的形状为 (样本数, 标注函数数),每个元素是对应 LF 对该样本的标注结果。
from snorkel.labeling import LFApplier
# 收集所有标注函数
lfs = [lf_positive_keywords, lf_negative_keywords, lf_sentiment_intensity, lf_negative_expressions, lf_review_length]
# 创建标签应用器
applier = LFApplier(lfs=lfs)
# 应用标注函数到训练集,生成标签矩阵
L_train = applier.apply(df=train_df)
# 查看标签矩阵形状
print("标签矩阵形状:", L_train.shape)
# 查看前5个样本的标注结果
print("前5个样本的标注结果:")
print(L_train[:5])输出的标签矩阵中,-1 表示弃权,0 表示负面,1 表示正面。例如 [1, -1, 1, -1, 0] 表示第一个 LF 标正面,第二个弃权,第三个标正面,第四个弃权,第五个标负面。
4.4 分析标注函数性能
在训练标签模型前,可通过 Snorkel 提供的工具分析标注函数的性能,包括覆盖率(Coverage)、冲突率(Conflict Rate)和重叠率(Overlap Rate):
- 覆盖率:标注函数对多少样本进行了标注(非弃权),覆盖率越高,函数的作用越大。
- 重叠率:两个标注函数同时对同一个样本标注的比例,重叠率过高可能表示函数冗余。
- 冲突率:两个标注函数对同一个样本标注不同标签的比例,冲突率过高需要优化标注函数。
from snorkel.labeling import analysis
# 计算标注函数的统计指标
lf_stats = analysis.LFAnalysis(L_train, lfs).lf_stats()
print(lf_stats)输出结果会展示每个 LF 的覆盖率、重叠率和冲突率,帮助我们筛选和优化标注函数。例如,若某个 LF 的覆盖率极低(如低于 5%),可以考虑删除或修改该函数。
4.5 训练标签模型
标签模型是 Snorkel 的核心,它无需真实标签,仅通过标签矩阵就能学习每个标注函数的可靠性权重,并输出概率化的训练标签。我们使用 LabelModel 类来训练标签模型:
from snorkel.labeling import LabelModel
# 初始化标签模型,设置类别数为2(正面/负面)
label_model = LabelModel(cardinality=2, verbose=True)
# 训练标签模型
label_model.fit(L_train=L_train, n_epochs=500, lr=0.001, log_freq=100)
# 对训练集生成概率化标签
Y_train_probs = label_model.predict_proba(L=L_train)
# 生成硬标签(概率大于0.5为正面,否则为负面)
Y_train_pred = label_model.predict(L=L_train)
# 查看生成的标签形状
print("概率化标签形状:", Y_train_probs.shape)
print("硬标签形状:", Y_train_pred.shape)
# 查看前5个样本的概率化标签和硬标签
print("前5个样本的概率化标签:", Y_train_probs[:5])
print("前5个样本的硬标签:", Y_train_pred[:5])概率化标签是一个二维数组,每一行对应一个样本,每一列对应一个类别的概率(如 [0.1, 0.9] 表示该样本为正面的概率是 0.9)。硬标签是基于概率的二分类结果,取值为 0 或 1。
4.6 训练下游分类模型
生成训练标签后,我们可以使用这些标签训练下游分类模型。本次实战使用 Scikit-learn 的逻辑回归模型作为下游模型,特征提取使用 TF-IDF 向量化器。
步骤1:特征提取
将文本数据转换为 TF-IDF 特征向量:
from sklearn.feature_extraction.text import TfidfVectorizer
# 初始化 TF-IDF 向量化器
vectorizer = TfidfVectorizer(stop_words="english", max_features=10000)
# 对训练集和测试集文本进行特征提取
X_train = vectorizer.fit_transform(train_df["text"])
X_test = vectorizer.transform(test_df["text"])
# 提取测试集真实标签(仅用于评估)
Y_test = test_df["sentiment"].values
print("训练集特征形状:", X_train.shape)
print("测试集特征形状:", X_test.shape)步骤2:训练下游模型
使用标签模型生成的硬标签训练逻辑回归模型:
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, classification_report
# 初始化逻辑回归模型
downstream_model = LogisticRegression(max_iter=1000)
# 使用弱监督标签训练模型
downstream_model.fit(X_train, Y_train_pred)
# 对测试集进行预测
Y_test_pred = downstream_model.predict(X_test)
# 评估模型性能
print(f"测试集准确率:{accuracy_score(Y_test, Y_test_pred):.4f}")
print("\n分类报告:")
print(classification_report(Y_test, Y_test_pred, target_names=["负面", "正面"]))输出的分类报告将包含准确率、精确率、召回率和 F1 分数等指标。在实际应用中,通过优化标注函数,模型性能可进一步提升。
步骤3:使用概率化标签训练模型(进阶)
除了硬标签,Snorkel 还支持使用概率化标签训练下游模型,这种方式可以保留标签的不确定性,通常能获得更好的性能。对于 Scikit-learn 模型,可通过 class_weight 参数实现;对于深度学习模型,可直接使用概率化标签作为损失函数的输入。
# 使用概率化标签调整类别权重
import numpy as np
# 计算每个样本的权重(概率的绝对值)
sample_weight = np.max(Y_train_probs, axis=1)
# 训练模型时加入样本权重
downstream_model_weighted = LogisticRegression(max_iter=1000)
downstream_model_weighted.fit(X_train, Y_train_pred, sample_weight=sample_weight)
# 评估加权模型性能
Y_test_pred_weighted = downstream_model_weighted.predict(X_test)
print(f"加权模型测试集准确率:{accuracy_score(Y_test, Y_test_pred_weighted):.4f}")五、进阶应用:实体识别任务
除了文本分类,Snorkel 还广泛应用于命名实体识别(NER)任务。NER 任务需要识别文本中的实体(如人名、地名、机构名),传统方法需要大量人工标注的序列数据,而 Snorkel 可通过编写序列标注函数,快速生成训练标签。
5.1 序列标注函数编写
在 NER 任务中,标注函数的输入是句子中的每个 token(词),输出是该 token 的实体标签(如 PER 表示人名,LOC 表示地名,O 表示非实体)。以下是一个简单的 NER 标注函数示例:
from snorkel.labeling import labeling_function
from snorkel.types import Token
# 定义实体标签常量
PER = 1 # 人名
LOC = 2 # 地名
O = 0 # 非实体
@labeling_function()
def lf_person_names(x: Token) -> int:
"""识别人名的标注函数,基于常见姓氏列表"""
common_last_names = ["Smith", "Johnson", "Williams", "Brown", "Jones"]
# 判断 token 是否为大写开头且在姓氏列表中
if x.text.istitle() and x.text in common_last_names:
return PER
return O
@labeling_function()
def lf_location_names(x: Token) -> int:
"""识别地名的标注函数,基于常见地名列表"""
common_locations = ["New York", "London", "Paris", "Tokyo", "Beijing"]
# 判断 token 是否为地名的一部分
if any(location in x.text for location in common_locations):
return LOC
return O5.2 序列标签模型训练
对于序列标注任务,Snorkel 提供了 SequenceLabelModel 类,专门用于处理序列数据的标签生成。其使用流程与文本分类类似,只需将标签应用器替换为 SequenceLFApplier:
from snorkel.labeling import SequenceLFApplier, SequenceLabelModel
# 收集序列标注函数
sequence_lfs = [lf_person_names, lf_location_names]
# 创建序列标签应用器
sequence_applier = SequenceLFApplier(lfs=sequence_lfs)
# 应用标注函数到序列数据集,生成序列标签矩阵
L_sequence = sequence_applier.apply(df=sequence_train_df)
# 初始化序列标签模型
sequence_label_model = SequenceLabelModel(cardinality=3, verbose=True)
# 训练模型
sequence_label_model.fit(L_sequence, n_epochs=100, lr=0.01)
# 生成序列标签
Y_sequence_probs = sequence_label_model.predict_proba(L_sequence)六、Snorkel 与深度学习框架的集成
Snorkel 可无缝对接 TensorFlow、PyTorch 等深度学习框架,用弱监督标签训练深度模型。以下是与 PyTorch 集成的示例,训练一个基于 LSTM 的文本分类模型:
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader
# 定义自定义数据集类
class TextDataset(Dataset):
def __init__(self, X, Y_probs):
self.X = torch.tensor(X.toarray(), dtype=torch.float32)
self.Y_probs = torch.tensor(Y_probs, dtype=torch.float32)
def __len__(self):
return len(self.X)
def __getitem__(self, idx):
return self.X[idx], self.Y_probs[idx]
# 定义 LSTM 分类模型
class LSTMClassifier(nn.Module):
def __init__(self, input_dim, hidden_dim, output_dim):
super().__init__()
self.lstm = nn.LSTM(input_dim, hidden_dim, batch_first=True)
self.fc = nn.Linear(hidden_dim, output_dim)
self.softmax = nn.Softmax(dim=1)
def forward(self, x):
# 调整输入形状为 (batch_size, seq_len, input_dim)
x = x.unsqueeze(1)
lstm_out, _ = self.lstm(x)
# 取最后一个时间步的输出
last_out = lstm_out[:, -1, :]
out = self.fc(last_out)
return self.softmax(out)
# 准备数据加载器
train_dataset = TextDataset(X_train, Y_train_probs)
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)
# 初始化模型、损失函数和优化器
input_dim = X_train.shape[1]
hidden_dim = 128
output_dim = 2
model = LSTMClassifier(input_dim, hidden_dim, output_dim)
criterion = nn.BCELoss() # 使用二元交叉熵损失
optimizer = optim.Adam(model.parameters(), lr=0.001)
# 训练模型
num_epochs = 10
for epoch in range(num_epochs):
model.train()
total_loss = 0.0
for batch_x, batch_y in train_loader:
optimizer.zero_grad()
outputs = model(batch_x)
loss = criterion(outputs, batch_y)
loss.backward()
optimizer.step()
total_loss += loss.item()
print(f"Epoch {epoch+1}/{num_epochs}, Loss: {total_loss/len(train_loader):.4f}")
# 评估模型
model.eval()
with torch.no_grad():
X_test_tensor = torch.tensor(X_test.toarray(), dtype=torch.float32)
Y_test_probs = model(X_test_tensor)
Y_test_pred = torch.argmax(Y_test_probs, dim=1).numpy()
print(f"LSTM 模型测试集准确率:{accuracy_score(Y_test, Y_test_pred):.4f}")七、相关资源链接
- PyPI 地址:https://pypi.org/project/snorkel
- Github 地址:https://github.com/snorkel-team/snorkel
- 官方文档地址:https://snorkel.readthedocs.io/en/latest/
关注我,每天分享一个实用的Python自动化工具。

