跳到主要内容

12 篇博文 含有标签「deep-learning」

查看所有标签

文本分类比赛学习记录

· 阅读需 6 分钟
Jason Lee

前言

本文是我对在公司参加的“ChatGPT 生成文本检测器”比赛。数据集为中文作文样本,其中从互联网上采集得到了真实作文,并且 ChatGLM-6B 生成了部分作文。参赛选手的任务是根据文本内容,区分作文的来源。但是,文本不是以内容呈现,而是一堆数字字符串,形如:[0 43 2 66]。可以推测出,每个数字代表一个汉字在语料库中的索引。

文本分类任务的四步:

  • 准备数据集:包括加载数据集和执行基本预处理,然后把数据集分为训练集和验证集。
  • 特征工程:将原始数据集被转换为用于训练机器学习模型的平坦特征(flat features)。
  • 模型训练
  • 进一步提高分类器性能

下面按照前面三步(省略第四步)介绍我的做法。

准备数据集

这一步的主要工作是读取原始文件,并划分训练集和验证集,用到的库 pandas 和 sklearn。

import pandas as pd
from sklearn import model_selection, preprocessing, naive_bayes, metrics, linear_model
from sklearn.feature_extraction.text import TfidfVectorizer, CountVectorizer

df = pd.read_csv('train.csv')

# print(df['content'])

#将数据集分为训练集和验证集
train_x, valid_x, train_y, valid_y = model_selection.train_test_split(df['content'], df['label'])

# label编码为目标变量
encoder = preprocessing.LabelEncoder()
train_y = encoder.fit_transform(train_y)
valid_y = encoder.fit_transform(valid_y)

特征工程

计数向量是数据集的矩阵表示,其中每行代表来自语料库的文档,每列表示来自语料库的术语,并且每个单元格表示特定文档中特定术语的频率计数:

#创建一个向量计数器对象
count_vect = CountVectorizer(analyzer='word', token_pattern=r'\w{1,}')
count_vect.fit(df['content'])

#使用向量计数器对象转换训练集和验证集
xtrain_count = count_vect.transform(train_x)
xvalid_count = count_vect.transform(valid_x)

模型训练

线性分类器用于训练。

## 训练函数
def train_model(classifier, feature_vector_train, label, feature_vector_valid, is_neural_net=False):
# fit the training dataset on the classifier
classifier.fit(feature_vector_train, label)


# predict the labels on validation dataset
predictions = classifier.predict(feature_vector_valid)


if is_neural_net:
predictions = predictions.argmax(axis=-1)


return classifier, metrics.accuracy_score(predictions, valid_y)

if __name__ == "__main__":
# classifier, accuracy = train_model(naive_bayes.MultinomialNB(), xtrain_count, train_y, xvalid_count)
# print("NB, Count Vectors: ", accuracy)

classifier, accuracy = train_model(linear_model.LogisticRegression(), xtrain_count, train_y, xvalid_count)
print("LR, Count Vectors: ", accuracy)

# csv out

testDF = pd.read_csv('test.csv')
test_x = testDF['content']

#使用向量计数器对象转换测试集
xtest_count = count_vect.transform(test_x)

predictions = classifier.predict(xtest_count)

submissionDF = pd.DataFrame()
submissionDF['name'] = testDF['name']
submissionDF['label'] = predictions

submissionDF.to_csv('LR_submission.csv', index=False)

总结

这次是我在 NLP 领域的一次小试牛刀,主要用到了 sklearn 框架,使得代码的编写变得简单。在不需要数据清洗等步骤的情况下,通过简单的计数向量和逻辑回归就使得最终的测试集结果达到了 0.99 以上,说明本次比赛的数据还是比较简单的。

另外,这次比赛也改变了我对于机器学习的认知。一开始我认为这是一个有难度的任务,想去 huggingface 上找一些文本分类的模型,践行“拿来主义”。但是怎么也找不到合适的。后来,在网上搜索文本分类的解决方案,看到一个相似的任务,且用了 sklearn,很短的代码就能达到不错的效果。也许,传统机器学习的能力比我想象的强很多。

更新

过了一段时间后,在接触更多 sklearn 知识后,我用 pipeline 来以更简单的方式实现:

import pandas as pd
from sklearn import model_selection, preprocessing, naive_bayes, metrics, linear_model
from sklearn.feature_extraction.text import TfidfVectorizer, CountVectorizer
from sklearn.decomposition import PCA, TruncatedSVD
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.svm import SVC
from sklearn.ensemble import VotingClassifier
from sklearn.neighbors import KNeighborsClassifier # 用于分类任务
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.metrics import confusion_matrix, precision_score, recall_score, f1_score
from sklearn.metrics import roc_curve, roc_auc_score
import matplotlib.pyplot as plt

df = pd.read_csv('train.csv')
X = df['content'].values
y = df['label'].values
le = preprocessing.LabelEncoder()
y = le.fit_transform(y)


#将数据集分为训练集和验证集
train_x, valid_x, train_y, valid_y = model_selection.train_test_split(X, y, random_state=43)

pipe_lr = Pipeline([('cv', CountVectorizer(analyzer='word', token_pattern=r'\w{1,}')),
('scl', StandardScaler(with_mean=False)),
('clf', linear_model.LogisticRegression(max_iter=50000))])

pipe_lr.fit(train_x, train_y)
print('Test Accuracy: %.4f' %pipe_lr.score(valid_x, valid_y))

## more metrics
valid_y_pred = pipe_lr.predict(valid_x)
confmat = confusion_matrix(y_true=valid_y, y_pred=valid_y_pred)
print(confmat)

print('Precision: %.3f' % precision_score(y_true=valid_y, y_pred=valid_y_pred))
print('Recall: %.3f' % recall_score(y_true=valid_y, y_pred=valid_y_pred))
print('F1: %.3f' % f1_score(y_true=valid_y, y_pred=valid_y_pred))

## draw roc
y_probs = pipe_lr.predict_proba(valid_x)
fpr, tpr, thresholds = roc_curve(valid_y, y_probs[:, 1], pos_label=1)
roc_auc = roc_auc_score(valid_y, y_probs[:, 1])
print('auc: %.4f' % roc_auc)
# Plot the ROC curve
plt.figure(figsize=(8, 6))
plt.plot(fpr, tpr, color='darkorange', lw=2, label='ROC curve (area = %.4f)' % roc_auc)
plt.plot([0, 1], [0, 1], color='navy', lw=2, linestyle='--')
plt.xlim([0.0, 1.0])
plt.ylim([0.0, 1.05])
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('Receiver Operating Characteristic')
plt.legend(loc='lower right')
plt.show()


## testset result

testDF = pd.read_csv('test.csv')
test_x = testDF['content'].values

pipe_lr.fit(X, y)
predictions = pipe_lr.predict(test_x)

submissionDF = pd.DataFrame()
submissionDF['name'] = testDF['name']
submissionDF['label'] = predictions

submissionDF.to_csv('LR_submission.csv', index=False)

在新版本中,加入了不少指标用于衡量模型性能。更重要的是,之前在输出测试结果时,用的是训练集上训练的模型,而不是整个数据集上重新训练一次。在重新训练后,发现指标从 0.9901 来到了 0.9912,提示非常显著。这提示了我以后一定要注意这个问题。

快速部署深度学习模型:gradio初体验

· 阅读需 6 分钟
Jason Lee

前言

需求:将机器学习的模型变成网页应用,并部署到公网上用于访问。

前几个月,当时也有类似的需求,想将自己毕业论文中的一个图像处理模型部署到公网。当时考虑的是,用 django 或者 flask 结合 pytorch 在本地搭一个应用,实现图片进出预训练模型的逻辑,然后用内网穿透的工具,使得外部可以访问到我的主机。但是这就带来许多问题,比如当时找了好几个内网穿透工具,要么收费,要么不好用。而且,主机总会有关机的时候,那么关机后服务也就停止了。一时间迟迟无法入手,当时就暂时放弃了。前阵子听说了 gradio 这个好东西,一个 python 包,通过几行代码,就可以在本地起一个应用,不仅带有简洁的 UI,而且内置了内网穿透。当时觉得很困难的问题,竟然一下迎刃而解了。感叹现在深度学习社区发展的速度。

gradio 本地运行

下面以知乎上的 rgb2gray 为例,讲解 gradio 库的使用。新建一个虚拟环境,安装上 gradio 包:

pip install -Uq gradio ## -U表示安装最新版本的包,-q表示静默安装,避免命令行污染

新建一个 app.py:

import gradio as gr
import cv2

def to_black(image):
output = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
return output

interface = gr.Interface(fn=to_black, inputs="image", outputs="image")
interface.launch(share=True)

运行 py 文件,gradio 在本地启动一个 HTTP 服务器,并通过内网穿透将该服务器暴露给公共互联网。局域网和公网链接都会输出在命令行终端。但是生成的公网链接会在 72 小时后过期,因此它是一个临时链接。如果要实现永久访问,需要将代码以及预训练模型(如有)上传到 huggingface space 上。

huggingface space

huggingface space 是一个类似 github 的地方,可以用来存 py 文件和预训练模型,并且在和 gradio 绑定后,可以实现免费的部署。如果是新电脑,运行gradio deploy,按指示添加 write token,绑定 huggingface 账号。也可以通过 git clone 命令,下载空间到本地再修改。新建 space 时的信息:

16886436128041688643612164.png

除了一个基础的 cpu 是免费的,其他都是按小时收费的。说实话还是蛮贵的,比如 T4 gpu,colab 和 kaggle 都是免费提供的,这里收 0.6 美刀/小时。当代码每次有更改时,空间会自动刷新并运行一次,可通过点击红框内的按钮查看进度:

16886441928151688644192174.png

成功之后,可以通过分享这个 space 的链接,来实现在线演示。

ps: 如何找到自己创建过的 space。要通过点击头像,进入个人主页。

访问速度对比

这里做一个简单的测试,本例的 rgb2gray 是一个非常简单的函数,cpu 都能瞬间完成,下面分别测试 gradio 在代码存在本地时通过 localhost 和内网穿透以及代码在 hf space 下的处理速度。图片尺寸为 1280*853,浏览器为 firefox。

方式时间(估计)使用场景
localhost0.4s本地演示给自己或者旁边的人看
内网穿透24s有外部访问的需求,且本地有 gpu,能覆盖网速带来的负面影响
hf space4s有外部访问或者长时间保持服务的需求,可以购买 gpu 时长

可以看出,内网穿透最慢,localhost 最快。也可以理解,内网穿透的网络传输方式最复杂,受网速波动影响大。

api

除了 gradio 提供的 UI,也可以自己设计 UI,并利用 gradio 提供的 api 接口,实现模型推理。但是,当我按照文档的代码运行时,遇到了报错:

ConnectionResetError: [WinError 10054] 远程主机强迫关闭了一个现有的连接。

现在暂时未解决。

结语

记录了 gradio 的初级用法,并给出了不同场景下的使用方案。有了 gradio,深度学习模型部署变得省事多了。

针对csv文件类型的深度学习全流程记录

· 阅读需 7 分钟
Jason Lee

前言

就像我之前的post里讲的那样,输入形式或者说处理对象的多样化是ai从业者面对的挑战之一。这篇文章,我将尽力解决存储在csv中的数据,涉及到读取、预处理、写入等环节。

读取

pd.read_csv('path')

预处理

一个csv文件,里面的值会是什么样呢?一种是数值形式,一种是文本形式。对于数值形式,有哪些处理方法呢?

缺失值处理:检查数据中是否存在缺失值(NaN或空值),并根据实际情况进行处理。常见的方法包括删除带有缺失值的样本、用平均值或中位数填充缺失值,或使用插值方法进行填充。

特征缩放:对于数值型特征,进行特征缩放可以帮助模型更好地进行训练。常见的特征缩放方法包括标准化(将数据转换为均值为0,方差为1的分布)、归一化(将数据缩放到0和1之间的范围)等。

异常值处理:检测和处理异常值可以提高模型的鲁棒性。可以使用统计方法(如均值加减三倍标准差)或基于分布的方法来检测异常值,并根据实际情况进行修正或删除。

离散化/分箱处理:将连续型的数值特征转化为离散型特征,可以在一定程度上提高模型的效果。可以使用等频分箱、等宽分箱等方法将数值范围划分为多个离散的区间,并将数值映射到对应的区间。

特征选择:根据数据的实际情况,选择对目标变量有更强相关性的数值特征进行训练。可以使用相关性分析、特征重要性评估等方法进行特征选择。

数据标准化:对于某些具有明确量纲的特征,例如日期、时间等,可以进行数据标准化,将其转换为合适的数值表示,便于模型理解和处理。

对于非数值型数据,可以采取以下预处理措施:

标签编码:对于具有顺序关系的分类变量,可以使用标签编码将其转换为数值表示。例如,将"低"、"中"、"高"转换为0、1、2等。

独热编码:对于没有顺序关系的分类变量,可以使用独热编码将其转换为多个二进制特征表示。独热编码将每个类别转换为一个独立的特征,并且只有一个特征的值为1,其余特征的值都为0。这可以避免模型将无序的类别变量视为有序的。

词袋模型/文本向量化:对于文本数据,可以使用词袋模型或文本向量化方法将文本转换为数值表示。词袋模型将文本视为单词的集合,并计算每个单词在文本中的出现频率或使用TF-IDF等方法进行加权。文本向量化方法(如Word2Vec、GloVe等)可以将单词嵌入到低维向量空间中,保留了单词之间的语义关系。

序列编码:对于具有顺序关系的序列数据,例如时间序列或序列文本,可以使用序列编码方法将其转换为数值表示。常见的序列编码方法包括循环神经网络(RNN)和长短期记忆网络(LSTM)等。

类别合并/分组:对于具有大量类别但类别之间相似度较高的非数值型数据,可以考虑将类别进行合并或分组,减少特征维度的同时保留信息。

异常值处理:对于非数值型数据,也可能存在异常值,例如错误的标签或不一致的类别。可以检测和处理这些异常值,例如进行纠正或删除。

标准化

numeric_features = all_features.dtypes[all_features.dtypes != 'object'].index
all_features[numeric_features] = all_features[numeric_features].apply(
lambda x: (x - x.mean()) / (x.std()))

数据集的制作

预处理好数据后,将数据转化为tensor格式。要将目前的pd表格数据转为tensor,最简单的方法是torch.tensor():

train_features = torch.tensor(all_features[:n_train].values, dtype=torch.float32)

这里不是采用继承torch.utils.Dataset类的写法,而是直接将train_features用一个函数制作数据集:

dataset = torch.utils.data.TensorDataset(*data)

最后包裹一层DataLoader。

搭建网络

对于网络,可以不用类的写法,而是实现nn.Sequential函数:

net = nn.Sequential(
nn.Linear(in_features, 512),
nn.ReLU(),
nn.Dropout(0.2),
nn.Linear(512, 1),
)

这样,节省了不少代码。

损失和优化器

这里的损失采用了MSE函数,由于期望值较大(六位数),因此损失值较大,让我以为训练出错了。另外,如果用SGD,损失将为nan。改用Adam优化器才能正常训练。推测是因为SGD对学习率敏感,学习率不设置的很精确的话,训练直接失败。

完整流程

形成一种规范:代码开头,导入库后,跟着指定默认设备,和超参数。接着开始制作数据集,包括数据的预处理。然后定义网络结构、损失函数、优化器。然后是正式的训练代码。最后测试时,记得将model调至测试模式,使得dropout能正常工作。

代码地址

总结

之前写pytorch的时候,写法很死板。经过这一次练习,接触到了新的写法,比如dataset部分和网络部分。

pytorch tutorial 笔记+注解

· 阅读需 9 分钟
Jason Lee

前言

pytorch,一个熟悉又陌生的朋友。以前用到它时,常常是哪里不会查哪里;等过一阵子又忘记了用法还得重新查。虽然毕业论文课题用到了框架,但是还处在拾人牙慧的阶段。这一次,经过前段时间 python 和 django 的学习,将提炼出来的学习经验用于 pytorch,先将官方文档的 tutorial 看完,然后看视频从零开始做一个项目。

Tensor

Tensor 和 numpy 的 array 很相似,意味着一些 api 的名字和功能相同。介绍 Tensor 的创建时,官网列举的来源包含:python list、numpy array、继承另一个 tensor 的形状和数据类型、随机/全 1/全 0。但是,在实际训练中,你面对的是这样的 Tensor:

batch_size = 64

# Create data loaders.
train_dataloader = DataLoader(training_data, batch_size=batch_size)
test_dataloader = DataLoader(test_data, batch_size=batch_size)

for X, y in test_dataloader:
print(f"Shape of X [N, C, H, W]: {X.shape}")
print(f"Shape of y: {y.shape} {y.dtype}")
break

X,y 都是 tensor。要理解 X 从哪里来的,我们从自定义 dataset 入手:

class CustomImageDataset(Dataset):
def __init__(self, annotations_file, img_dir, transform=None, target_transform=None):
self.img_labels = pd.read_csv(annotations_file)
self.img_dir = img_dir
self.transform = transform
self.target_transform = target_transform

def __len__(self):
return len(self.img_labels)

def __getitem__(self, idx):
img_path = os.path.join(self.img_dir, self.img_labels.iloc[idx, 0])
image = read_image(img_path)
label = self.img_labels.iloc[idx, 1]
if self.transform:
image = self.transform(image)
if self.target_transform:
label = self.target_transform(label)
return image, label

read_image 的全称是 torchvision.io.read_image,将 jpeg 或者 png 读取为 uint8 的 tensor。在别的实例中,有可能在transform中添加一个 ToTensor(),ToTensor converts a PIL image or NumPy ndarray into a FloatTensor. and scales the image’s pixel intensity values in the range [0., 1.].

tensor 的属性只有三个:shape、dtype、device。其中 shape 和 size()经常搞混,得到错误提示:xxx 数据类型没有 shape 属性/size()方法,这里对他们做一个区分。

在 Python 和 PyTorch 中,以下数据类型具有 shape 属性: NumPy 数组(numpy.ndarray):NumPy 是一个用于科学计算的 Python 库,其数组具有 shape 属性。可以使用 ndarray.shape 来获取数组的形状。 PyTorch 张量(torch.Tensor):PyTorch 是一个深度学习框架,其中的张量对象具有 shape 属性。可以使用 tensor.shape 来获取张量的形状。 需要注意的是,PyTorch 的张量类型包括 CPU 张量和 CUDA 张量,它们都具有 shape 属性。 在 PyTorch 中,size()方法和 shape 属性实际上是等价的,它们都用于获取张量的形状。因此,在 PyTorch 张量上使用 size()方法和使用 shape 属性将返回相同的结果。 在 PyTorch 中,shape 和 size()都返回一个 torch.Size 对象,它是一个元组子类,可以像元组一样进行索引操作。例如,对于形状为(2, 3)的张量,可以通过 shape[0]或 size()[0]来访问第一个维度的大小。 对于其他 Python 数据类型,如元组、列表或字符串,没有 shape 属性或 size()方法,可以使用 len()函数来获取元素的数量或长度。

数据加载

数据加载需要两个类:Dataset 和 DataLoader:

from torch.utils.data import Dataset, DataLoader

Dataset

根据数据集的特点进行数据加载、预处理和标准化等操作。以前面的自定义 dataset 为例,它首先继承了 Dataset 类,然后分别实现了init, len, getitem方法。其中,getitem方法是最重要的,它可以实现文件从硬盘到内存的读取,然后转化为 tensor,以及可选的 transform 预处理步骤。

Dataloader

DataLoader是一个数据加载器类,用于将 Dataset 中的数据分批加载到模型中进行训练或推理。它提供了多线程数据加载、批处理和数据随机打乱等功能。通过使用 DataLoader,你可以方便地迭代整个数据集,并按照指定的批次大小获取数据。

DataLoader 类接收一个 Dataset 对象作为参数,并可以配置以下参数:

  • batch_size:指定每个批次的样本数量。
  • shuffle:指定是否对数据进行随机打乱。
  • num_workers:指定用于数据加载的线程数。

最重要的是,DataLoader 是一种可迭代序列,可以用 next 进行数据的读取:

train_features, train_labels = next(iter(train_dataloader))

在实际代码中,常使用 enumerate()函数。它返回一个生成器对象,该生成器生成索引-元素对:

for batch, (X, y) in enumerate(dataloader):
X, y = X.to(device), y.to(device)

在上面这个例子中,batch 就是第几个 batch 的意思,(X, y)就是 Dataset 返回的元素和标签。所以上述代码将遍历一次整个数据集,按 batch 返回数据和 batch 的索引。

在这里可以引入 epoch 和 batch 的概念。epoch 表示完整扫描整个数据集的次数,batchsize 表示每次 epoch 中,计算一次损失需要读取的样本数量。而 batch 就是样本总数除以 batchsize 的取整。所以,一个 epoch 中会有若干个 batch。

批次大小的选择涉及到多个因素,包括内存限制、计算资源、模型性能等。较大的批次大小可以提高计算效率,但可能需要更多的内存,并且可能导致模型的收敛速度变慢。较小的批次大小可以减少内存占用,但可能导致计算效率降低。 通常,选择适当的批次大小需要进行实验和调整。一般而言,较大的批次大小在具有较大训练集和较强计算能力的情况下可以获得更好的性能,而较小的批次大小对于内存受限的情况或者需要更好的模型泛化能力的情况可能更合适。

读取数据时的 cpu 和 gpu

Dataset 中,数据通常会被预处理、转换为张量等操作,并在 CPU 上进行。然后,通过 DataLoader 将处理后的数据以指定的批量大小加载到内存中,并返回一个或多个批量的迭代器。在训练过程中,可以将这些批量数据移动到 GPU 上,并将其传递给模型进行训练或推断。完整的示例代码:

import torch
from torch.utils.data import Dataset, DataLoader

class MyDataset(Dataset):
def __init__(self, data):
self.data = data

def __len__(self):
return len(self.data)

def __getitem__(self, index):
# 数据预处理和转换操作在CPU上进行
sample = self.data[index]
processed_sample = preprocess(sample)
tensor_sample = torch.tensor(processed_sample, dtype=torch.float32)
return tensor_sample

# 创建数据集
dataset = MyDataset(data)

# 创建数据加载器
batch_size = 32
dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=True)

# 检查GPU是否可用
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

# 别忘了将网络模型也放到gpu中(如果可用)
model = Model().to(device)

# 在训练过程中,将批量数据移动到GPU上进行计算
for batch in dataloader:
inputs = batch.to(device)
targets = get_targets(batch) # 假设有一个函数用于获取目标值
targets = targets.to(device)

# 在GPU上进行模型的训练或推断
outputs = model(inputs)
loss = loss_function(outputs, targets)
optimizer.zero_grad()
loss.backward()
optimizer.step()

Batch Normalization

· 阅读需 7 分钟

前言

Batch Normalization (BN)层,通过将数据批量归一化(使其分布在 N(0,1)),有下列好处:

缓解了梯度传递问题,使模型适应更大的学习率,加速了训练; 改善了饱和非线性模型不易训练的问题; 还起到了正则化的作用。

可以看出,BN 和之前学到的 Xavier 初始化权重的目的类似,都是使训练更容易。

实现

BN 的公式表达为:

$$\mathrm{BN}(\mathbf{x}) = \boldsymbol{\gamma} \odot \frac{\mathbf{x} - \hat{\boldsymbol{\mu}}_\mathcal{B}}{\hat{\boldsymbol{\sigma}}_\mathcal{B}} + \boldsymbol{\beta}.$$

$gamma$ $beta$都是可学习的参数,它们使得 BN 层像一个只有一个神经元的线性层。

import torch
from torch import nn
from d2l import torch as d2l


def batch_norm(X, gamma, beta, moving_mean, moving_var, eps, momentum):
# 通过is_grad_enabled来判断当前模式是训练模式还是预测模式
if not torch.is_grad_enabled():
# 如果是在预测模式下,直接使用传入的移动平均所得的均值和方差
X_hat = (X - moving_mean) / torch.sqrt(moving_var + eps)
else:
assert len(X.shape) in (2, 4)
if len(X.shape) == 2:
# 使用全连接层的情况,计算特征维上的均值和方差
mean = X.mean(dim=0)
var = ((X - mean) ** 2).mean(dim=0)
else:
# 使用二维卷积层的情况,计算通道维上(axis=1)的均值和方差。
# 这里我们需要保持X的形状以便后面可以做广播运算
mean = X.mean(dim=(0, 2, 3), keepdim=True)
var = ((X - mean) ** 2).mean(dim=(0, 2, 3), keepdim=True)
# 训练模式下,用当前的均值和方差做标准化
X_hat = (X - mean) / torch.sqrt(var + eps)
# 更新移动平均的均值和方差
moving_mean = momentum * moving_mean + (1.0 - momentum) * mean
moving_var = momentum * moving_var + (1.0 - momentum) * var
Y = gamma * X_hat + beta # 缩放和移位
return Y, moving_mean.data, moving_var.data

class BatchNorm(nn.Module):
# num_features:完全连接层的输出数量或卷积层的输出通道数。
# num_dims:2表示完全连接层,4表示卷积层
def __init__(self, num_features, num_dims):
super().__init__()
if num_dims == 2:
shape = (1, num_features)
else:
shape = (1, num_features, 1, 1)
# 参与求梯度和迭代的拉伸和偏移参数,分别初始化成1和0
self.gamma = nn.Parameter(torch.ones(shape))
self.beta = nn.Parameter(torch.zeros(shape))
# 非模型参数的变量初始化为0和1
self.moving_mean = torch.zeros(shape)
self.moving_var = torch.ones(shape)

def forward(self, X):
# 如果X不在内存上,将moving_mean和moving_var
# 复制到X所在显存上
if self.moving_mean.device != X.device:
self.moving_mean = self.moving_mean.to(X.device)
self.moving_var = self.moving_var.to(X.device)
# 保存更新过的moving_mean和moving_var
Y, self.moving_mean, self.moving_var = batch_norm(
X, self.gamma, self.beta, self.moving_mean,
self.moving_var, eps=1e-5, momentum=0.9)
return Y

预测和训练时 BN 层的作用方式不同。主要是因为训练时 BN 层是根据批量数据的均值方差来估计整个数据集的均值方差。训练是用了一个滑动估计下来的保留均值方差,BN 层用它来在测试时归一化。

我的实践

在我的弱光照增强网络中,Unet 结构在卷积层中也大量采用了 BN 层。由于显存的限制(8G),我的 batch_size 是 8,EnlightenGAN 作者设为 32。我的担心是,batch_size 设的太小会不会导致 BN 层失效?chatgpt 的回答是:

即使你的 batch_size 很小,使用批量归一化(Batch Normalization)也是有益的,因为它可以使神经网络训练更加稳定和快速。以下是几个原因: 减少内部协变量偏移(Internal Covariate Shift):内部协变量偏移是指在每一层输入的分布随着网络的训练而发生变化,这会导致网络训练变得困难。批量归一化通过在每个小批量上标准化输入,可以有效地减少内部协变量偏移的影响。 提高梯度传播:批量归一化可以缩小每层的激活值范围,使得梯度更容易传播,从而加快训练速度。 增强正则化:批量归一化可以在每个小批量上对激活进行标准化,这相当于对每个激活值都进行了一定的正则化,从而可以减少过拟合的风险。 虽然 batch_size 较小时批量归一化的效果可能不如 batch_size 较大时显著,但仍然可以提高神经网络的性能。因此,我建议在训练神经网络时,无论 batch_size 的大小,都使用批量归一化。

如果将 batch_size 设置为 1,那么使用批量归一化(Batch Normalization)的效果会受到影响,因为它需要在每个小批量上计算均值和方差来进行标准化。当 batch_size 为 1 时,每个小批量只有一个样本,这样计算出来的均值和方差可能不太准确,因此批量归一化的效果可能会降低。 不过,对于某些特殊的任务或架构,有些人可能会在 batch_size 为 1 时使用批量归一化。这时可以考虑使用“自适应批量归一化”(Adaptive Batch Normalization)或“实例归一化”(Instance Normalization)等变种方法,它们可以在 batch_size 为 1 时进行标准化。但需要注意的是,这些方法可能不如标准批量归一化在大批量数据上的效果好,所以在选择是否使用它们时需要根据具体情况进行权衡。

所以,应该还是可以用的。

卷积

· 阅读需 2 分钟

前言

我的研究生方向是图像处理,也用到了深度学习工具,所以这部分相对熟悉一些。在学习过程中,也发现了一些以前没有注意到的知识,比如卷积这个词是怎么从数学转到神经网络的一种操作的。

感受野

卷积核是在局部窗口内操作的,所以,卷积核的大小反映了学习到的范围。浅层的时候,卷积核的感受野只有图像的一小部分;随着网络的变深,后面的卷积核能看到前面的,其感受野最终能大等于整张图。

填充和步幅

目前图像处理最常用的卷积核大小、填充和步幅组合是(3,1,1)。这样的卷积不会使输出特征图的尺寸发生变化。如果要使图像的尺寸缩小,还可以用池化层。

填充可以增加输出的高度和宽度。这常用来使输出与输入具有相同的高和宽。步幅可以减小输出的高和宽,例如输出的高和宽仅为输入的高和宽的 1/𝑛(𝑛 是一个大于 1 的整数)。

pytorch的基本使用

· 阅读需 4 分钟

之前我们有大量的从零实现一层网络,现在借用先进的pytorch框架,让很多功能得到封装,可以快捷的组建一个块。

nn.Module

nn.Module是pytorch一切网络的祖宗。例如,可以用nn.Sequential()搭建,也可以新写一个类:

class net(nn.Module):
def __init__(self):
super.__init__()
...

def forward(self, x):
...

nn.Sequential和net类都是继承于nn.Module。

参数管理

参数的访问用state_dict()函数:

print(net[2].state_dict())
OrderedDict([('weight', tensor([[ 0.3016, -0.1901, -0.1991, -0.1220,  0.1121, -0.1424, -0.3060,  0.3400]])), ('bias', tensor([-0.0291]))])

每一种参数(比如weight)都是一个类,下面包含数值,梯度等属性。比如:

net[2].weight.data, net[2].weight.grad

权重初始化

默认情况下,PyTorch会根据一个范围均匀地初始化权重和偏置矩阵, 这个范围是根据输入和输出维度计算出的。 PyTorch的 nn.init模块提供了多种预置初始化方法。用nn.init:

def init_normal(m):
if type(m) == nn.Linear:
nn.init.normal_(m.weight, mean=0, std=0.01)
nn.init.zeros_(m.bias)
net.apply(init_normal)
net[0].weight.data[0], net[0].bias.data[0]

m就是层的意思,apply函数会将init_normal函数遍历处理所有层。上述代码将所有的全连接层权重初始化为N~(0.0.01)的高斯分布。

共享权重

共享权重似乎在论文中见过很多次,就是将一个子网络训练的过程中,将权重分享给另一个相同结构的子网络。这里给出的,是两个全连接层的参数共享:

# 我们需要给共享层一个名称,以便可以引用它的参数
shared = nn.Linear(8, 8)
net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(),
shared, nn.ReLU(),
shared, nn.ReLU(),
nn.Linear(8, 1))
net(X)
# 检查参数是否相同
print(net[2].weight.data[0] == net[4].weight.data[0])
net[2].weight.data[0, 0] = 100
# 确保它们实际上是同一个对象,而不只是有相同的值
print(net[2].weight.data[0] == net[4].weight.data[0])

创建全新的层

有时候,可能需要创建一个pytorch未实现的层。以自定义一个linear层为例:

class MyLinear(nn.Module):
def __init__(self, in_units, units):
super().__init__()
self.weight = nn.Parameter(torch.randn(in_units, units))
self.bias = nn.Parameter(torch.randn(units,))
def forward(self, X):
linear = torch.matmul(X, self.weight.data) + self.bias.data
return F.relu(linear)

核心步骤就是在init函数,设置好可以更新的权重,然后再forward函数里面定义计算方式。

权重的保存(checkpoint)

pytorch只能保存权重,而不能连同网络结构一起保存,不过听说tf可以。保存方式是torch.save():

假设要保存参数的对应模型是一个MLP:

class MLP(nn.Module):
def __init__(self):
super().__init__()
self.hidden = nn.Linear(20, 256)
self.output = nn.Linear(256, 10)

def forward(self, x):
return self.output(F.relu(self.hidden(x)))

net = MLP()
X = torch.randn(size=(2, 20))
Y = net(X)
torch.save(net.state_dict(), 'mlp.params') # 保存
net.load_state_dict(torch.load('mlp.params'))  # 加载

数值稳定性和权重初始化

· 阅读需 3 分钟

梯度爆炸和梯度消失问题

梯度爆炸

因为梯度的计算是通过偏导数的链式法则,所以,对于一个很深的网络,反向传播时,计算最后几层的梯度,很可能会超出数值的边界。比如cuda限制了16位的浮点数运算。这时,$1.1^100$超过了数值上界,程序就会报错。

梯度消失

如果某几个中间层的梯度很小,接近于0,那么前面几个层的梯度也是0,权重参数就得不到更新,这就是梯度消失。Sigmoid函数求导后,两头的梯度都接近于0,所以,很容易发生梯度消失。

合理初始化权重

合理初始化权重,可以缓解梯度爆炸和梯度消失问题。合理初始化化权重,就好比在选择一个离终点(最优点)近,且好走的起点。具体做法是,假设每层的输出和权重都满足独立正态分布,且均值、方差都相等。这种假设有以下几个原因(chatgpt):

避免梯度消失或梯度爆炸:如果每一层的输出和权重具有相同的均值和方差,那么它们传递的梯度也会有相同的大小,这可以避免梯度消失或梯度爆炸问题。

提高网络的收敛速度:假设每一层的输出和权重具有相同的均值和方差,可以使网络更容易收敛,因为权重初始化过大或过小可能会使得网络的训练变得非常缓慢。

减少过拟合的可能性:如果权重初始化过大,网络容易出现过拟合现象,而假设每一层的输出和权重具有相同的均值和方差可以避免这种情况的发生。

多层感知机(MLP)

· 阅读需 3 分钟

终于从前面的单层网络linear-regression和softmax过渡到多层神经网络了。为了对更加复杂的数据进行学习,多层感知机将多个全连接层叠加,并增加激活函数。激活函数是非线性的,因为如果不加激活函数或者激活函数线性,那么多层神经网络还是遵循线性规律,等同于一层。

多层感知机

中间的,就是隐藏层。所以,多加的隐藏层中神经元的个数也是一个超参数。

从零实现代码

import torch
from torch import nn
from d2l import torch as d2l

batch_size=256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
num_inputs, num_outputs, num_hiddens = 784, 10, 784

W1 = nn.Parameter(torch.randn(num_inputs, num_hiddens, requires_grad=True) * 0.01)
b1 = nn.Parameter(torch.zeros(num_hiddens, requires_grad=True))
W2 = nn.Parameter(torch.randn(num_hiddens, num_outputs, requires_grad=True) * 0.01)
b2 = nn.Parameter(torch.zeros(num_outputs, requires_grad=True))

params = [W1, b1, W2, b2]
def relu(X):
a = torch.zeros_like(X)
return torch.max(X, a)
def net(X):
X = X.reshape((-1, num_inputs))
H = relu(X@W1 + b1)
return (H@W2 + b2)
loss = nn.CrossEntropyLoss(reduction='none')
num_epochs, lr = 10, 0.1
updater = torch.optim.SGD(params, lr=lr)
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, updater)
d2l.predict_ch3(net, test_iter, n=20)

利用pytorch api实现

import torch
from torch import nn
from d2l import torch as d2l
## 网络搭建
net = nn.Sequential(nn.Flatten(),
nn.Linear(784, 256),
nn.ReLU(),
nn.Linear(256, 10))

## 初始化权重
def init_weights(m):
if type(m)==nn.Linear:
nn.init.normal_(m.weight, std=0.01)

net.apply(init_weights);
## 超参数设置
batch_size, lr, num_epochs = 256, 0.1, 10
## 损失函数
loss = nn.CrossEntropyLoss(reduction='none')
## 优化器
trainer = torch.optim.SGD(net.parameters(), lr=lr)
## 加载数据(dataloader)
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
## 正式训练
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)

总结

深度学习代码编写步骤:

网络搭建-》初始化权重-》超参数设置-》损失函数-》优化器-》加载数据-》正式训练

softmax回归(分类)

· 阅读需 2 分钟

回归和分类

机器学习(深度学习)的任务纷繁复杂。最基础的是回归和分类。回归是预测连续值,分类是预测离散类别。

分类问题是多输出,因此,训练标签和模型(网络)的输出应该是多维的。独热编码 (one-hot encoding)是一种表示多分类的方式。就是在一个向量中,将真实分类索引下的值设为 1,其他是 0,因此向量自身的内积为 1。另外,模型的线性表示为:

所以,softmax 也是一个单层全连接网络。


\begin{aligned}
o*1 &= x_1 w*{11} + x*2 w*{12} + x*3 w*{13} + x*4 w*{14} + b*1,\\
o_2 &= x_1 w*{21} + x*2 w*{22} + x*3 w*{23} + x*4 w*{24} + b*2,\\
o_3 &= x_1 w*{31} + x*2 w*{32} + x*3 w*{33} + x*4 w*{34} + b_3.
\end{aligned}

softmax 函数

分类问题的训练,可以用最大化正确分类的概率来表示。因此,要通过 softmax 函数将神经网络的输出$o_i$转化为 0-1 之间的概率。

\begin{equation}
\hat{\mathbf{y}} = \mathrm{softmax}(\mathbf{o})\quad \text{其中}\quad \hat{y}_j = \frac{\exp(o_j)}{\sum_k \exp(o_k)}
\end{equation}

softmax 可以保证输出在 0-1 之间,且 softmax 可导。