鼓舞运用基线模型进行迭代建模。

Intuition

基线是为迭代开发铺平道路的简略基准:

  • 由于模型杂乱度低,经过超参数调整进行快速试验。
  • 发现数据问题、错误假设、代码中的错误等,由于模型自身并不杂乱。
  • 帕累托准则:可以用最少的初始努力实现杰出的功能。

进程

这是建立基线的高档办法:

  1. 从最简略的基线开端,以比较后续开发。这一般是一个随机(时机)模型。
  2. 运用 IFTTT、辅佐数据等开发根据规矩的办法(假如或许)。
  3. _经过处理_约束和_鼓励_表明和模型架构来渐渐添加杂乱性。
  4. 权衡功能基线之间的_权衡_(功能、延迟、巨细等)。
  5. 跟着数据集的增长,重新拜访和迭代基线。

要考虑的权衡

在挑选要进行的模型架构时,需求考虑哪些重要的权衡?怎么优先考虑它们?

显示答案

这些权衡的优先级取决于您的上下文。

  • performance`:考虑粗粒度和细粒度(例如每类)功能。
  • latency:您的模型对推理的响应速度有多快。
  • size: 你的模型有多大,你能支持它的存储吗?
  • compute:练习你的模型需求多少本钱(美元、碳脚印等)?
  • interpretability: 你的模型需求解释它的猜测吗?
  • bias checks:您的模型是否经过了关键误差查看?
  • time to develop: 你需求多长时刻来开发第一个版本?
  • time to retrain: 重新练习你的模型需求多长时刻?假如您需求经常进行再培训,这一点十分重要。
  • maintenance overhead:保护模型版本需求谁和什么,由于 ML 的真实作业是在部署 v1 之后开端的。您不能像许多团队对传统软件所做的那样,将其交给您的站点可靠性团队来保护它。

迭代数据

还可以在您的数据集上设置基线。不要运用固定的数据集并在模型上迭代,而是挑选一个好的基线并在数据集上迭代:

  • 删去或修复数据样本(误报和否定)
  • 准备和转化特征
  • 扩大或巩固班级
  • 兼并辅佐数据集
  • 识别要提高的共同切片

分布式练习

需求为应用程序做的一切练习都发生在一个作业人员和一个加速器(CPU/GPU)上,可是,会考虑对十分大的模型或在处理大型数据集时进行分布式练习。分布式练习或许涉及:

  • 数据并行性:作业人员收到较大数据集的不同切片。
    • 同步练习运用AllReduce聚合梯度并在每批结束时更新一切作业人员的权重(同步)。
    • 异步练习运用通用参数服务器来更新权重,由于每个作业人员都在其数据片上进行练习(异步)。
  • 模型并行性:一切作业人员运用相同的数据集,但模型在它们之间拆分(与数据并行性比较更难以实现,由于很难从反向传播中别离和组合信号)。

应用分布式练习有很多挑选,例如 PyTorch 的分布式包、Ray、Horovd等。

优化

当数据或模型太大而无法练习时,分布式练习策略十分有用,可是当模型太大而无法部署时呢?以下模型压缩技能一般用于使大型模型适合现有根底架构:

  • 修剪:删去权重(非结构化)或整个通道(结构化)以减小网络的巨细。目标是保持模型的功能,同时添加其稀少性。
  • 量化:经过降低权重的精度(例如 32 位到 8 位)来削减权重的内存占用。或许会失去一些精度,但它不应该对功能发生太大影响。
  • 蒸馏:练习较小的网络以“仿照”较大的网络,办法是让它重现较大网络层的输出。

建模基线模型

在神经网络中提取常识 [来源]

基线

每个应用程序的基线轨迹因使命而异。关于应用程序,将遵循以下路径:

  1. 随机的
  2. 根据规矩
  3. 简略机器学习

将激发对缓慢添加表明(例如文本向量化)和架构(例如逻辑回归)的杂乱性的需求,并处理每个进程的约束。

假如您不熟悉此处的建模概念,请必须查看根底课程。

note

运用的特定模型与本 MLOps 课程无关,由于首要关注将模型投入生产和保护所需的一切组件。因而,在持续学习本note本之后的其他课程时,请随意挑选任何model。

将首先设置一些将在不同基线试验中运用的函数。

import random
def set_seeds(seed=42):
    """Set seeds for reproducibility."""
    np.random.seed(seed)
    random.seed(seed)
def preprocess(df, lower, stem, min_freq):
    """Preprocess the data."""
    df["text"] = df.title + " " + df.description  # feature engineering
    df.text = df.text.apply(clean_text, lower=lower, stem=stem)  # clean text
    # Replace OOS tags with `other`
    oos_tags = [item for item in df.tag.unique() if item not in ACCEPTED_TAGS]
    df.tag = df.tag.apply(lambda x: "other" if x in oos_tags else x)
    # Replace tags below min_freq with `other`
    tags_above_freq = Counter(tag for tag in tags.elements()
                            if (tags[tag] >= min_freq))
    df.tag = df.tag.apply(lambda tag: tag if tag in tags_above_freq else None)
    df.tag = df.tag.fillna("other")
    return df
def get_data_splits(X, y, train_size=0.7):
    """Generate balanced data splits."""
    X_train, X_, y_train, y_ = train_test_split(
        X, y, train_size=train_size, stratify=y)
    X_val, X_test, y_val, y_test = train_test_split(
        X_, y_, train_size=0.5, stratify=y_)
    return X_train, X_val, X_test, y_train, y_val, y_test

数据集很小,因而将运用整个数据集进行练习,但关于较大的数据集,应该一直在一个小子集上进行测验(在必要时进行改组之后),这样就不会在计算上浪费时刻。

df = df.sample(frac=1).reset_index(drop=True)  # shuffle
df = df[: num_samples]  # None = all samples

需求洗牌吗?

为什么打乱数据集很重要?

显示答案

_需求_打乱数据,由于数据是按时刻次序组织的。与前期项目比较,最新项目或许具有某些流行的功能或标签。假如在创立数据拆分之前不进行洗牌,那么模型将只会在较早的信号上进行练习而且无法泛化。可是,在其他情况下(例如时刻序列猜测),洗牌会导致数据泄露。

随机的

动机:想知道随机(时机)体现是什么样的。一切的努力都应该远远高于这个基线。

from sklearn.metrics import precision_recall_fscore_support
# Set up
set_seeds()
df = pd.read_csv("labeled_projects.csv")
df = df.sample(frac=1).reset_index(drop=True)
df = preprocess(df, lower=True, stem=False, min_freq=min_freq)
label_encoder = LabelEncoder().fit(df.tag)
X_train, X_val, X_test, y_train, y_val, y_test = \
    get_data_splits(X=df.text.to_numpy(), y=label_encoder.encode(df.tag))
# Label encoder
print (label_encoder)
print (label_encoder.classes)

<LabelEncoder(num_classes=4)> [‘computer-vision’, ‘mlops’, ‘natural-language-processing’, ‘other’]

# Generate random predictions
y_pred = np.random.randint(low=0, high=len(label_encoder), size=len(y_test))
print (y_pred.shape)
print (y_pred[0:5])

(144,) [0 0 0 1 3]

# Evaluate
metrics = precision_recall_fscore_support(y_test, y_pred, average="weighted")
performance = {"precision": metrics[0], "recall": metrics[1], "f1": metrics[2]}
print (json.dumps(performance, indent=2))

{ “precision”: 0.31684880006233446, “recall”: 0.2361111111111111, “f1”: 0.2531624273393283 }

假设每个类别都有相同的概率。让运用练习拆分来找出真实的概率是多少。

# Class frequencies
p = [Counter(y_test)[index]/len(y_test) for index in range(len(label_encoder))]
p

[0.375, 0.08333333333333333, 0.4027777777777778, 0.1388888888888889]

# Generate weighted random predictions
y_pred = np.random.choice(a=range(len(label_encoder)), size=len(y_test), p=p)
# Evaluate
metrics = precision_recall_fscore_support(y_test, y_pred, average="weighted")
performance = {"precision": metrics[0], "recall": metrics[1], "f1": metrics[2]}
print (json.dumps(performance, indent=2))

{ “precision”: 0.316412540257649, “recall”: 0.3263888888888889, “f1”: 0.31950372012322 }

约束:没有运用输入中的符号来影响猜测,所以没有学到任何东西。

根据规矩

动机:期望在输入中运用信号(以及领域专业常识和辅佐数据)来确定标签。

# Setup
set_seeds()
df = pd.read_csv("labeled_projects.csv")
df = df.sample(frac=1).reset_index(drop=True)
df = preprocess(df, lower=True, stem=False, min_freq=min_freq)
label_encoder = LabelEncoder().fit(df.tag)
X_train, X_val, X_test, y_train, y_val, y_test = \
    get_data_splits(X=df.text.to_numpy(), y=label_encoder.encode(df.tag))
def get_tag(text, aliases_by_tag):
    """If a token matches an alias,
    then add the corresponding tag class."""
    for tag, aliases in aliases_by_tag.items():
        if replace_dash(tag) in text:
            return tag
        for alias in aliases:
            if alias in text:
                return tag
    return None
# Sample
text = "A pretrained model hub for popular nlp models."
get_tag(text=clean_text(text), aliases_by_tag=aliases_by_tag)

‘natural-language-processing’

# Prediction
tags = []
for text in X_test:
    tag = get_tag(text, aliases_by_tag=aliases_by_tag)
    tags.append(tag)
# Encode labels
y_pred = [label_encoder.class_to_index[tag] if tag is not None else -1 for tag in tags]
# Evaluate
metrics = precision_recall_fscore_support(y_test, y_pred, average="weighted")
performance = {"precision": metrics[0], "recall": metrics[1], "f1": metrics[2]}
print (json.dumps(performance, indent=2))

{ “precision”: 0.9097222222222222, “recall”: 0.18055555555555555, “f1”: 0.2919455654201417 }

为什么召回率这么低?

为什么准确率很高,但召回率却如此之低?

显示答案

当输入信号中没有运用这些特定的别号时,仅依赖别号会证明是灾难性的。为了改善这一点,可以构建一个包含相关术语的词袋。例如,将诸如text classification和之类的术语映射named entity recognitionnatural-language-processing标签,但构建它是一项不平凡的使命。更不用说,跟着数据环境的老练,需求不断更新这些规矩。

# Pitfalls
text = "Transfer learning with transformers for text classification."
print (get_tag(text=clean_text(text), aliases_by_tag=aliases_by_tag))

Tip

还可以运用词干提取来进一步完善根据规矩的流程:

from nltk.stem import PorterStemmer
stemmer = PorterStemmer()
print (stemmer.stem("democracy"))
print (stemmer.stem("democracies")
)

可是这些根据规矩的办法只能在肯定条件匹配时发生具有高确定性的标签,因而最好不要在这种办法上花费太多精力。

约束:未能概括或学习任何隐式模式来猜测标签,由于将输入中的符号视为孤立的实体。

矢量化

动机

  • 表明:运用词频-逆文档频率(TF-IDF)来捕获某个符号相关于一切输入对特定输入的重要性,而不是将输入文本中的单词视为孤立的符号。
  • 架构:期望模型可以有意义地提取编码信号以猜测输出标签。

到目前为止,现已将输入文本中的单词视为孤立的符号,而且还没有真实捕捉到符号之间的任何含义。让运用 TF-IDF(经过 Scikit-learn’s TfidfVectorizer)来捕获令牌对特定输入相关于一切输入的重要性。

wi,j=tfi,j∗log(Ndfi)w_{i, j} = \text{tf}_{i, j} * log(\frac{N}{\text{df}_i})

建模基线模型

from sklearn.feature_extraction.text import TfidfVectorizer
# Setup
set_seeds()
df = pd.read_csv("labeled_projects.csv")
df = df.sample(frac=1).reset_index(drop=True)
df = preprocess(df, lower=True, stem=False, min_freq=min_freq)
label_encoder = LabelEncoder().fit(df.tag)
X_train, X_val, X_test, y_train, y_val, y_test = \
    get_data_splits(X=df.text.to_numpy(), y=label_encoder.encode(df.tag))
# Saving raw X_test to compare with later
X_test_raw = X_test
# Tf-idf
vectorizer = TfidfVectorizer(analyzer="char", ngram_range=(2,7))  # char n-grams
print (X_train[0])
X_train = vectorizer.fit_transform(X_train)
X_val = vectorizer.transform(X_val)
X_test = vectorizer.transform(X_test)
print (X_train.shape)  # scipy.sparse.csr_matrix

tao large scale benchmark tracking object diverse dataset tracking object tao consisting 2 907 high resolution videos captured diverse environments half minute long (668, 99664)

# Class weights
counts = np.bincount(y_train)
class_weights = {i: 1.0/count for i, count in enumerate(counts)}
print (f"class counts: {counts},\nclass weights: {class_weights}")

class counts: [249 55 272 92], class weights: {0: 0.004016064257028112, 1: 0.01818181818181818, 2: 0.003676470588235294, 3: 0.010869565217391304}

数据不平衡

关于数据集,或许经常会注意到数据不平衡问题,其间一系列接连值(回归)或某些类别(分类)或许没有足够的数据量可供学习。这在练习时成为一个首要问题,由于模型将学习泛化到可用数据并在数据稀少的区域体现欠安。有几种技能可以缓解数据不平衡,包含重采样、兼并类权重、增强等。尽管抱负的处理方案是为少量类收集更大都据!

将运用imblearn 包来保证对少量类进行过采样以等于大都类(带有大大都样本的标签)。

pip install imbalanced-learn==0.8.1 -q
from imblearn.over_sampling import RandomOverSampler
# Oversample (training set)
oversample = RandomOverSampler(sampling_strategy="all")
X_over, y_over = oversample.fit_resample(X_train, y_train)

warning

重要的是,仅在练习拆分上应用采样,这样就不会在其他数据拆分中引进数据走漏。

class counts: [272 272 272 272], class weights: {0: 0.003676470588235294, 1: 0.003676470588235294, 2: 0.003676470588235294, 3: 0.003676470588235294}

约束

  • 表明:TF-IDF 表明不封装超出频率的太多信号,但需求更细粒度的令牌表明。
  • 架构:期望开发可以以更契合上下文的方法运用更好表明的编码的模型。

机器学习

将运用随机梯度下降分类器 ( SGDClassifier ) 作为模型。将运用对数损失,以便它有用地与 SGD 进行逻辑回归。

这样做是由于期望对练习进程(epochs)有更多的控制,而不是运用 scikit-learn 的默许二阶优化办法(例如LGBFS)进行逻辑回归。

from sklearn import metrics
from sklearn.linear_model import SGDClassifier
from sklearn.metrics import log_loss, precision_recall_fscore_support
# Initialize model
model = SGDClassifier(
    loss="log", penalty="l2", alpha=1e-4, max_iter=1,
    learning_rate="constant", eta0=1e-1, power_t=0.1,
    warm_start=True)
# Train model
num_epochs = 100
for epoch in range(num_epochs):
    # Training
    model.fit(X_over, y_over)
    # Evaluation
    train_loss = log_loss(y_train, model.predict_proba(X_train))
    val_loss = log_loss(y_val, model.predict_proba(X_val))
    if not epoch%10:
        print(
            f"Epoch: {epoch:02d} | "
            f"train_loss: {train_loss:.5f}, "
            f"val_loss: {val_loss:.5f}"
        )

Epoch: 00 | train_loss: 1.16930, val_loss: 1.21451 Epoch: 10 | train_loss: 0.46116, val_loss: 0.65903 Epoch: 20 | train_loss: 0.31565, val_loss: 0.56018 Epoch: 30 | train_loss: 0.25207, val_loss: 0.51967 Epoch: 40 | train_loss: 0.21740, val_loss: 0.49822 Epoch: 50 | train_loss: 0.19615, val_loss: 0.48529 Epoch: 60 | train_loss: 0.18249, val_loss: 0.47708 Epoch: 70 | train_loss: 0.17330, val_loss: 0.47158 Epoch: 80 | train_loss: 0.16671, val_loss: 0.46765 Epoch: 90 | train_loss: 0.16197, val_loss: 0.46488

可以进一步优化练习管道,例如提早中止将运用创立的验证集的功能。可是期望在建模阶段简化这个与模型无关的课程

warning

SGDClassifier有一个标志,您可以在其间early_stopping指定要用于验证的练习集的一部分。为什么这对来说是个坏主意?由于现已在练习会集应用了过采样,所以假如这样做,会引进数据走漏。

# Evaluate
y_pred = model.predict(X_test)
metrics = precision_recall_fscore_support(y_test, y_pred, average="weighted")
performance = {"precision": metrics[0], "recall": metrics[1], "f1": metrics[2]}
print (json.dumps(performance, indent=2))

{ “precision”: 0.8753577441077441, “recall”: 0.8680555555555556, “f1”: 0.8654096949533866 }

Tip

Scikit-learn 有一个称为管道的概念,它答应将转化和练习进程组合到一个可调用函数中。

可以从头开端创立管道:

# Create pipeline from scratch
from sklearn.pipeline import Pipeline
steps = (("tfidf", TfidfVectorizer()), ("model", SGDClassifier()))
pipe = Pipeline(steps)
pipe.fit(X_train, y_train)

或运用练习有素的组件制作一个:

# Make pipeline from existing components
from sklearn.pipeline import make_pipeline
pipe = make_pipeline(vectorizer, model)

约束

  • 表明:TF-IDF 表明没有封装太多频率以外的信号,但需求更细粒度的令牌表明,以阐明令牌自身的重要性(嵌入)。
  • 架构:期望开发可以以更契合上下文的方法运用更好表明的编码的模型。
# Inference (with tokens similar to training data)
text = "Transfer learning with transformers for text classification."
y_pred = model.predict(vectorizer.transform([text]))
label_encoder.decode(y_pred)

[‘natural-language-processing’]

# Probabilities
y_prob = model.predict_proba(vectorizer.transform([text]))
{tag:y_prob[0][i] for i, tag in enumerate(label_encoder.classes)}

{‘computer-vision’: 0.023672281234089494, ‘mlops’: 0.004158589896756235, ‘natural-language-processing’: 0.9621906411391856, ‘other’: 0.009978487729968667}

# Inference (with tokens not similar to training data)
text = "Interpretability methods for explaining model behavior."
y_pred = model.predict(vectorizer.transform([text]))
label_encoder.decode(y_pred)

[‘natural-language-processing’]

# Probabilities
y_prob = model.predict_proba(vectorizer.transform([text]))
{tag:y_prob[0][i] for i, tag in enumerate(label_encoder.classes)}

{‘computer-vision’: 0.13150802188532523, ‘mlops’: 0.11198040241517894, ‘natural-language-processing’: 0.584025872986128, ‘other’: 0.17248570271336786}

将创立一个自定义猜测函数,假如大都类不高于某个 softmax 分数,则猜测other该类。在目标中,认为精度对来说十分重要,可以利用标签和 QA 作业流程来提高后续手动查看期间的召回率。

warning

模型或许会受到过度自信的影响,因而应用此约束或许不如想象的那么有用,尤其是关于更大的神经网络。有关更多信息,请参阅评价课程的自信学习部分。

# Determine first quantile softmax score for the correct class (on validation split)
y_pred = model.predict(X_val)
y_prob = model.predict_proba(X_val)
threshold = np.quantile([y_prob[i][j] for i, j in enumerate(y_pred)], q=0.25)  # Q1
threshold

0.6742890218960005

warning

在验证拆分中履行此操作十分重要,因而不会在评价测验拆分之前运用练习拆分或走漏信息来夸张值。

# Custom predict function
def custom_predict(y_prob, threshold, index):
    """Custom predict function that defaults
    to an index if conditions are not met."""
    y_pred = [np.argmax(p) if max(p) > threshold else index for p in y_prob]
    return np.array(y_pred)
def predict_tag(texts):
    y_prob = model.predict_proba(vectorizer.transform(texts))
    other_index = label_encoder.class_to_index["other"]
    y_pred = custom_predict(y_prob=y_prob, threshold=threshold, index=other_index)
    return label_encoder.decode(y_pred)
# Inference (with tokens not similar to training data)
text = "Interpretability methods for explaining model behavior."
predict_tag(texts=[text])

[‘other’]

# Evaluate
y_prob = model.predict_proba(X_test)
y_pred = custom_predict(y_prob=y_prob, threshold=threshold, index=other_index)
metrics = precision_recall_fscore_support(y_test, y_pred, average="weighted")
performance = {"precision": metrics[0], "recall": metrics[1], "f1": metrics[2]}
print (json.dumps(performance, indent=2))

{ “precision”: 0.9116161616161617, “recall”: 0.7569444444444444, “f1”: 0.7929971988795519 }

Tip

乃至可以运用每个类别的阈值,特别是由于有一些数据不平衡,这会影响模型对某些类别的决心。

y_pred = model.predict(X_val)
y_prob = model.predict_proba(X_val)
class_thresholds = {}
for index in range(len(label_encoder.classes)):
    class_thresholds[index] = np.mean(
        [y_prob[i][index] for i in np.where(y_pred==index)[0]])

这门 MLOps 课程实际上与模型无关(只要它发生概率分布),因而可以随意运用更杂乱的表明(嵌入)和更杂乱的架构(CNN、transformers等)。将在其余课程中运用这个基本的逻辑回归模型,由于它简略、快速而且实际上具有适当的功能(与最先进的预练习transformer比较,f1 差异<10%)。


本文主体源自以下链接:

@article{madewithml,
    author       = {Goku Mohandas},
    title        = { Made With ML },
    howpublished = {\url{https://madewithml.com/}},
    year         = {2022}
}

本文由mdnice多渠道发布