社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  机器学习算法

fast.ai 深度学习笔记:第一部分第五课

ApacheCN_飞龙 • 7 年前 • 884 次点击  

一,引言

没有足够的关于结构化深度学习的出版物,但它肯定出现在行业中:

结构化深度学习,作者:Kerem Turgutlu @datascience.com

你可以使用此工具从 Google 下载图片并解决自己的问题:

小型图像数据集的乐趣(第2部分),作者:Nikhil B @datascience.com

如何训练神经网络的介绍(一篇伟大的技术写作):

我们如何“训练”神经网络?,由 Vitaly Bushaev @datascience.com

学生们在 Kaggle 幼苗分类比赛中与 Jeremy 竞争。

II. 协同过滤 - 使用 MovieLens 数据集

讨论的笔记本可以在这里找到(lesson5-movielens.ipynb)。

我们来看看数据。 我们将使用userId (类别), movieId (类别)和rating(因变量)进行建模。

ratings = pd.read_csv(path+'ratings.csv')
ratings.head() 

为 Excel 创建子集

我们创建了最受欢迎的电影和大多数电影狂热粉的交叉表,我们将其复制到 Excel 中进行可视化。

g=ratings.groupby('userId')['rating'].count()  
topUsers=g.sort_values(ascending=False)[:15] 
g=ratings.groupby('movieId')['rating'].count()  
topMovies=g.sort_values(ascending=False)[:15] 
top_r = ratings.join(topUsers, rsuffix='_r', how='inner', on='userId')
top_r = top_r.join(topMovies, rsuffix='_r', how='inner', on='movieId') 
pd.crosstab(top_r.userId, top_r.movieId, top_r.rating, aggfunc=np.sum) 

这是包含上述信息的 excel 文件。 首先,我们将使用矩阵分解而不构建神经网络。

  • 蓝色单元格 - 实际评级
  • 紫色单元格 - 我们的预测
  • 红色单元格 - 我们的损失函数即均方根误差(RMSE)
  • 绿色单元格 - 电影嵌入(随机初始化)
  • 橙色单元格 - 用户嵌入(随机初始化)

每个预测是电影嵌入向量和用户嵌入向量的点积。 在线性代数术语中,它等于矩阵乘积,因为一个是行,一个是列。 如果没有实际评级,我们将预测设置为零(将其视为测试数据 - 而不是训练数据)。

然后我们使用梯度下降来减少损失。 Microsoft Excel 在加载项中有一个“求解器”,可以通过更改所选单元格来最小化变量(GRG Nonlinear是你要使用的方法)。

这可称为“浅学习”(与深度学习相反),因为没有非线性层或第二线性层。 那么我们直觉上做了什么呢? 每部电影的五个数字称为“嵌入”(潜在因式) - 第一个数字可能代表科幻和幻想的程度,第二个数字可能是电影使用了多少特效,第三个可能是对话驱动的程度。与之类似,每个用户还有 5 个数字,例如,表示用户喜欢幻想,特效和对话驱动的电影的程度。 我们的预测是这些向量的叉乘。 由于我们没有每个用户的每个电影评论,因此我们试图找出哪些电影与这部电影相似,以及其他用户评价其他电影,如何与这个用户评价这个电影类似(因此称为“协同”)。

我们如何处理新用户或新电影 - 我们是否需要重新训练模型? 我们现在没有时间来讨论这个问题,但基本上你需要有一个新的用户模型或最初会使用的新电影模型,随着时间的推移你需要重新训练模型。

简单的 Python 版本 [26:03]

这应该看起来很熟悉了。 我们通过选择随机 ID 集来创建验证集。 wd是 L2 正则化的权重衰减, n_factors是我们想要的嵌入矩阵有多大。

val_idxs = get_cv_idxs(len(ratings)) 
wd = 2e-4 
n_factors = 50

我们从 CSV 文件创建模型数据对象:

cf = CollabFilterDataset.from_csv(path, 'ratings.csv', 'userId', 'movieId', 'rating')

然后我们得到一个适合模型数据的学习器,并拟合模型:

learn = cf.get_learner(n_factors, val_idxs, 64, opt_fn=optim.Adam)
learn.fit(1e-2, 2, wds=wd, cycle_len=1, cycle_mult=2)
learn.fit(1e-2, 2, wds=wd, cycle_len=1, cycle_mult=2) 

输出 MSE

由于输出是均方误差,你可以通过以下方式获取 RMSE:

math.sqrt(0.765) 

输出约为 0.88,优于 0.91 的基准。

你可以通过常规方式获得预测:

preds = learn.predict() 

你也可以使用 seaborn sns(建立在matplotlib之上):

y = learn.data.val_y
sns.jointplot(preds, y, kind='hex', stat_func=None)

使用 Python 的点乘

T是 PyTorch 中的张量

a = T([[1., 2], [3, 4]])
b = T([[2., 2], [10, 10]])

当我们在 numpy 或 PyTorch 中的张量之间有一个数学运算符时,它将逐元素运算,假设它们都具有相同的维数。 下面是你如何计算两个向量的点积(例如(1,2)·(2,2)= 6 - 矩阵a和b的第一行):

(a*b).sum(1)

'''
6
70
[torch.FloatTensor of size 2]
'''

构建我们的第一个自定义层(即 PyTorch 模块)[33:55]

我们通过创建一个扩展nn.Module并覆盖forward函数的 Python 类来实现它。




    
class DotProduct (nn.Module):
   def forward(self, u, m): return (u*m).sum(1)

现在我们可以调用它并得到预期的结果(注意我们不需要写model.forward(a, b)来调用forward函数 - 它是 PyTorch 魔法。) [40:14] :

model = DotProduct()
model(a,b)

'''
6
70
[torch.FloatTensor of size 2]
'''

建造更复杂的模块 [41:31]

这个实现对DotProduct类有两个补充:

  • 两个nn.Embedding矩阵
  • 在上面的嵌入矩阵中查找我们的用户和电影

用户 ID 很可能不是连续的,这使其很难用作嵌入矩阵的索引。 因此,我们将首先创建从零开始并且连续的索引,并使用匿名函数lambda和 Pandas 的apply函数,将ratings.userId列替换为索引,并对ratings.movieId执行相同的操作。

u_uniq = ratings.userId.unique() 
user2idx = {o:i for i,o in enumerate(u_uniq)} 
ratings.userId = ratings.userId.apply(lambda x: user2idx[x])  

m_uniq = ratings.movieId.unique() 
movie2idx = {o:i for i,o in enumerate(m_uniq)} 
ratings.movieId = ratings.movieId.apply(lambda x: movie2idx[x])  

n_users=int(ratings.userId.nunique()) n_movies=int(ratings.movieId.nunique())

提示: {o:i for i,o in enumerate(u_uniq)}是一个方便的代码行,保存在你的工具箱中!

class EmbeddingDot(nn.Module):
    def __init__(self, n_users, n_movies):
        super().__init__()
        self.u = nn.Embedding(n_users, n_factors)
        self.m = nn.Embedding(n_movies, n_factors)
        self.u.weight.data.uniform_(0,0.05)
        self.m.weight.data.uniform_(0,0.05)
        
    def forward(self, cats, conts):
        users,movies = cats[:,0],cats[:,1]
        u,m = self.u(users),self.m(movies)
        return (u*m).sum(1)

请注意, __init__是一个现在需要的构造函数,因为我们的类需要跟踪“状态”(多少部电影,多少用户,多少因子等)。 我们将权重初始化为 0 到 0.05 之间的随机数,你可以在这里找到关于权重初始化的标准算法的更多信息,“Kaiming Initialization”(PyTorch 有 He 初始化实用函数,但是我们试图从头开始) [46:58] 。

Embedding不是张量而是变量 。 变量执行与张量完全相同的操作,但它也可以自动微分。 要从变量中拉出张量,请调用data属性。 所有张量函数都有尾随下划线的变体(例如uniform_ ),将原地执行。

x = ratings.drop(['rating', 'timestamp'],axis=1)
y = ratings['rating'].astype(np.float32)
data = ColumnarModelData.from_data_frame(path, val_idxs, x, y, ['userId', 'movieId'], 64)

我们正在复用 Rossmann 笔记本中的 ColumnarModelData(来自 fast.ai 库),这也是EmbeddingDot类 [50:20] 中的def forward(self, cats, conts)函数中,存在分类和连续变量的原因。 由于在这种情况下我们没有连续变量,我们将忽略conts并使用cats的第一和第二列作为users和movies 。 请注意,它们是小批量的用户和电影。 重要的是不要手动遍历小批量,因为你不会获得 GPU 加速,而是一次处理整个小批量,正如上面的forward功能的第 3 和第 4 行中你看到的那样 [51:00-52:05]。

wd=1e-5
model = EmbeddingDot(n_users, n_movies).cuda()
opt = optim.SGD(model.parameters(), 1e-1, weight_decay=wd, momentum=0.9)

optim为 PyTorch 提供优化器。 model.parameters()是从nn.Modules继承的函数之一,它为我们提供了更新/学习的权重。

fit(model, data, 3, opt, F.mse_loss) 

这个函数来自 fast.ai 库 [54:40] 并且比我们一直在使用的learner.fit()更接近常规的 PyTorch 方法。 它不会为你提供诸如“带有重启的随机梯度下降”或开箱即用的“可微分学习率”等功能。

让我们改进我们的模型

偏差 - 适应普遍流行的电影或普遍热情的用户。

min_rating,max_rating = ratings.rating.min(),ratings.rating.max()
min_rating,max_rating

def get_emb(ni,nf):
    e = nn.Embedding(ni, nf)
    e.weight.data.uniform_(-0.01,0.01)
    return e

class EmbeddingDotBias(nn.Module):
    def __init__(self, n_users, n_movies):
        super().__init__()
        (self.u, self.m, self.ub, self.mb) = [get_emb(*o) for o in [
            (n_users, n_factors), (n_movies, n_factors), (n_users,1), (n_movies,1)
        ]]
        
    def forward(self, cats, conts):
        users,movies = cats[:,0],cats[:,1]
        um = (self.u(users)* self.m(movies)).sum(1)
        res = um + self.ub(users).squeeze() + self.mb(movies).squeeze()
        res = F.sigmoid(res) * (max_rating-min_rating) + min_rating
        return res

squeeze是 PyTorch 版本的广播 [1:04:11] 请参阅机器学习课程或 numpy 文档,来获取更多信息。

我们可以压缩评级,使其在 1 到 5 之间吗?是!通过 sigmoid 函数进行预测将导致数字位于 1 和 0 之间。因此,在我们的情况下,我们可以将其乘以 4 并加 1 - 这将导致 1 到 5 之间的数字。

F是 PyTorch 函数( torch.nn.functional ),包含用于张量的所有函数,在大多数情况下作为F导入。

wd=2e-4
model = EmbeddingDotBias(cf.n_users, cf.n_items).cuda()
opt = optim.SGD(model.parameters(), 1e-1, weight_decay=wd, momentum=0.9)
fit(model, data, 3, opt, F.mse_loss)

'''
[ 0.       0.85056  0.83742]                                     
[ 1.       0.79628  0.81775]                                     
[ 2.       0.8012   0.80994]
'''

让我们来看看我们在简单的 Python 版本中,使用的 fast.ai 代码 [1:13:44]。在column_data.py文件中,CollabFilterDataSet.get_leaner调用get_model函数,该函数创建EmbeddingDotBias类,与我们创建的相同。

神经网络版 [1:17:21]

我们回到 excel 表来理解直觉。 请注意,我们创建user_idx来查找嵌入,就像我们之前在 python 代码中所做的那样。 如果我们对user_idx进行单热编码并将其乘以用户嵌入,我们将为用户获取对应的行。 如果它只是矩阵乘法,为什么我们需要嵌入? 它用于计算表现优化的目的。

我们不计算用户嵌入向量和电影嵌入向量的点积来得到预测,而是将两者连接起来并使用神经网络来提供它。

class EmbeddingNet(nn.Module):
    def __init__(self, n_users, n_movies, nh=10, p1=0.5, p2=0.5):
        super().__init__()
        (self.u, self.m) = [get_emb(*o) for o in [
            (n_users, n_factors), (n_movies, n_factors)]]
        self.lin1 = nn.Linear(n_factors*2, nh)
        self.lin2 = nn.Linear(nh, 1)
        self.drop1 = nn.Dropout(p1)
        self.drop2 = nn.Dropout(p2)
        
    def forward(self, cats, conts):
        users,movies = cats[:,0],cats[:,1]
        x = self.drop1(torch.cat([self.u(users),self.m(movies)], dim=1))
        x = self.drop2(F.relu(self.lin1(x)))
        return F.sigmoid(self.lin2(x)) * (max_rating-min_rating+1) + min_rating-0.5

请注意,我们不再有偏差项,因为 PyTorch 中的Linear层已经存在偏差。 nh是线性层创建的激活数量(Jeremy 称之为“隐藏数量”)。

它只有一个隐藏层,所以可能不是“深层”,但这绝对是一个神经网络。

wd=1e-5
model = EmbeddingNet(n_users, n_movies).cuda()
opt = optim.Adam(model.parameters(), 1e-3, weight_decay=wd)
fit(model, data, 3, opt, F.mse_loss)

'''
A Jupyter Widget
[ 0.       0.88043  0.82363]                                    
[ 1.       0.8941   0.81264]                                    
[ 2.       0.86179  0.80706]
'''

请注意,损失函数也在F中(这里,它是均方损失)。

既然我们有神经网络,我们可以尝试很多东西:

  • 添加 Dropout
  • 为用户嵌入和电影嵌入使用不同的嵌入大小
  • 不仅是用户和电影嵌入,而且还附加来自原始数据的电影类型嵌入和/或时间戳。
  • 增加/减少隐藏层数和激活个数
  • 增加/减少正则化

训练循环中发生了什么?[ 1:33:21]

目前,我们正在将权重更新传递给 PyTorch 的优化器。 优化器有什么作用? 什么是momentum ?

opt = optim.SGD(model.parameters(), 1e-1, weight_decay=wd, momentum=0.9)

我们将在 excel 表(graddesc.xlsm)中实现梯度下降 - 从右到左看工作表。 首先我们创建一个随机的x,y与x线性相关(例如y = a * x + b)。 通过使用x和y的集合,我们将尝试学习a和b。

要计算误差,我们首先需要预测,并计算差的平方:

为了减少误差,我们增加/减少a和b一点点,并找出会导致误差减小的东西。 这被称为通过有限差分找到导数。

有限差分在高维空间中变得复杂 [1:41:46] ,并且它变得非常耗费内存并且需要很长时间。 所以我们想找到一些方法来更快地完成这项工作。 值得查找 Jacobian 和 Hessian 之类的东西(深度学习书: 第 84 页第 4.3.1 节 )。

链式规则和反向传播

更快的方法是解析地做到这一点 [1:45:27] 。 为此,我们需要一个链式规则:

链规则概述

这是 Chris Olah 的不错的文章,作为链式规则的反向传播。

现在我们用 WolframAlpha 给出的实际导数替换有限差分(注意有限差分输出与实际导数非常接近,如果你需要计算自己的导数,那么它是个快速健全性检查的好方法):

  • “在线”训练 - 大小为 1 的小批量

这就是你如何使用 excel 表格进行 SGD。 如果你要使用 CNN 电子表格的输出更改预测值,我们可以使用 SGD 训练 CNN。

动量 [1:53:47]

来吧,接受这个暗示 - 这是一个很好的方向。 请这样做下去,做得更多。

使用这种方法,我们将在当前的小批量导数,和我们在最后一个小批量(单元格 K9)之后采取的步骤(和方向)之间使用线性插值:

与de/db相比(它的符号(+/-)是随机的),具有动量的那个将继续向同一方向移动一点点直到某一点。 这将减少训练所需的迭代。

Adam [1:59:04]

Adam 的速度要快得多,但问题在于最终的预测并不像带动量的 SGD 那样好。 似乎这是由于 Adam 和权重衰减的联合使用。 解决此问题的新版本称为 AdamW。

  • cell J8:导数和前一个方向的线性插值(与我们在动量中的相同)
  • cell L8:来自最后一步( cell L7 )的导数平方,加上导数平方的线性插值
  • 这个想法被称为“指数加权移动平均”(换句话说,平均值与之前的值相乘)

学习率比以前高得多,因为我们将它除以L8的平方根。

如果你看一下 fast.ai 库(model.py),你会注意到在fit函数中,它不只是计算平均损失,而是计算损失的指数加权移动平均。

avg_loss = avg_loss * avg_mom + loss * (1-avg_mom) 

另一个有用的概念是每当你看到α(...) + (1-α)(...)时,立即想到线性插值。

一些直觉

  • 我们计算了梯度平方的指数加权移动平均值,取其平方根,并将学习率除以它。
  • 梯度平方总是正的。
  • 当梯度的方差很大时,梯度平方将很大。
  • 当梯度恒定时,梯度平方将很小。
  • 如果梯度变化很大,我们要小心并将学习率除以大数(减速)
  • 如果梯度变化不大,我们将通过将学习率除以较小的数字来采取更大的步骤
  • 自适应学习率 - 跟踪梯度平方的平均值,并使用它来调整学习率。 因此,只有一种学习风格,但如果梯度是恒定的,每个迭代的每个参数都会跳跃得更大;否则会跳得更小。
  • 有两个参数 - 一个用于梯度,另一个用于梯度平方(在 PyTorch 中,它被称为 beta,它是两个数字的元组)

AdamW [2:11:18]

当参数多于数据点时,正则化变得很重要。 我们以前见过 Dropout ,权重衰减是另一种正则化。 权重衰减(L2 正则化)通过将权重平方(乘以权重衰减因子)加到损失中来惩罚大权重。 现在损失函数想要保持较小的权重,因为增加权重会增加损失;因此,只有当损失的改善超过惩罚时才这样做。

问题在于,由于我们将权重平方添加到损失函数,这会影响梯度的移动平均值和 Adam 的梯度平方的移动平均值。 这导致当梯度变化很大时减少权重衰减量,并且当变化很小时增加权重衰减量。 换句话说,“惩罚大权重,如果梯度变化不大”,这不是我们想要的。 AdamW 从损失函数中删除了权重衰减,并在更新权重时直接添加它。


今天看啥 - 高品质阅读平台
本文地址:http://www.jintiankansha.me/t/T0HUBYs4gQ
Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/25980