机器学习算法高级教程-三-

120 阅读1小时+

机器学习算法高级教程(三)

原文:Pro Machine Learning Algorithms

协议:CC BY-NC-SA 4.0

十、循环神经网络

在第九章中,我们看了卷积神经网络(CNN)如何改进传统的图像分类神经网络架构。尽管细胞神经网络在处理图像平移和旋转的图像分类中表现很好,但它们不一定有助于识别时间模式。本质上,人们可以认为 CNN 是识别静态模式。

循环神经网络(RNNs)旨在解决识别时间模式的问题。

在本章中,您将学习以下内容:

  • RNN 的工作细节
  • 在 RNN 使用嵌入
  • 使用 RNN 生成文本
  • 使用 RNN 进行情感分类
  • 从 RNN 搬到 LSTM

RNN 可以用多种方式来设计。一些可能的方法如图 10-1 所示。

A463052_1_En_10_Fig1_HTML.png

图 10-1

RNN examples

在图 10-1 中注意以下几点:

  • 底部的方框是输入
  • 中间的方框是隐藏层
  • 顶部的方框是输出

所示一对一架构的一个例子是我们在第七章中看到的典型神经网络,在输入和输出层之间有一个隐藏层。一对多 RNN 架构的一个例子是输入图像并输出图像的标题。多对一 RNN 架构的一个例子可以是作为输入给出的电影评论和作为输出的电影情感(正面、负面或中性评论)。最后,多对多 RNN 架构的一个例子是从一种语言到另一种语言的机器翻译。

理解架构

让我们看一个例子,更仔细地看看 RNN 建筑。我们的任务如下:“给定一串单词,预测下一个单词。”我们将尝试预测“这是一个 _____”后面的单词。假设实际的句子是“这是一个例子。”

传统的文本挖掘技术将以下列方式解决这个问题:

  1. 对每个单词进行编码,如果需要,为额外的单词留出空间:

    This: {1,0,0,0}
    is: {0,1,0,0}
    an: {0,0,1,0}
    
    
  2. 对句子进行编码:

    "This is an": {1,1,1,0}
    
    
  3. 创建训练数据集:

    Input --> {1,1,1,0}
    Output --> {0,0,0,1}
    
    
  4. 建立一个有输入和输出的模型。

这里的一个主要缺点是,如果输入句子是“这是一个”或“这是一个”或“这是一个”,输入表示不会改变。我们知道,其中每一个都非常不同,不能用数学上的相同结构来表示。

这种认识要求有不同的架构,一个看起来更像图 10-2 的架构。

A463052_1_En_10_Fig2_HTML.png

图 10-2

A change in the architecture

在图 10-2 所示的架构中,句子中的每个单词都放在三个输入框中的一个单独的框中。此外,由于“this”进入第一个框,“is”进入第二个框,“an”进入第三个框,因此句子的结构得以保留。

输出“示例”应该在顶部的输出框中。

解读 RNN

我们可以认为 RNN 是一种保存记忆的机制,其中记忆包含在隐藏层中。如图 10-3 所示。

A463052_1_En_10_Fig3_HTML.png

图 10-3

Memory in the hidden layer

图 10-3 中右边的网络是左边网络的展开版本。左边的网络是一个传统的网络,有一个变化:隐藏层在连接到输入的同时连接到自身(隐藏层是图中的圆)。

请注意,当隐藏层与输入层一起连接到自身时,它将连接到隐藏层的“以前版本”和当前输入层。我们可以把这种隐藏层被连接回自身的现象看作是 RNN 创造记忆的机制。

权重 U 表示将输入层连接到隐藏层的权重,权重 W 表示隐藏层到隐藏层的连接,权重 V 表示隐藏层到输出层的连接。

Why Store Memory?

需要存储记忆,因为在前面的例子中和一般的文本生成中,下一个单词不一定依赖于前面的单词,而是依赖于该单词前面的几个单词的上下文来预测。

鉴于我们正在看前面的单词,应该有一种方法将它们保存在内存中,这样我们就可以更准确地预测下一个单词。此外,我们还应该按顺序进行记忆——通常情况下,在预测下一个单词时,较新的单词比距离被预测的单词较远的单词更有用。

RNN 的工作细节

请注意,典型的神经网络有一个输入层,然后在隐藏层激活,然后在输出层激活 softmax。RNN 是相似的,但是有记忆。我们再来看另一个例子:“这是一个例子”。给定一个输入“This”,我们被期望预测“is”,类似地,对于一个输入“is”,我们被期望得出一个“an”的预测和一个作为输入的“an”的“example”的预测。该数据集在 github 中以“RNN 维度直觉. xlsx”的名称提供。

编码的输入和输出字如下:

A463052_1_En_10_Figa_HTML.jpg

RNN 的结构如图 10-4 所示。

A463052_1_En_10_Fig4_HTML.png

图 10-4

The RNN structure

让我们来解构每个相关权重矩阵的维度:

A463052_1_En_10_Figb_HTML.jpg

wxh 随机初始化,维数为 4 × 3。每个输入的尺寸为 1 × 4。因此,隐藏层是输入和 wxh 之间的矩阵乘法,每个输入行的维数为 1 × 3。预期的输出是句子中紧挨着输入单词的单词的一次性编码版本。请注意,最后一个预测“空白”是不准确的,因为我们的预期输出都是 0。理想情况下,我们应该在一键编码版本中有一个新的列来处理所有看不见的单词。然而,为了理解 RNN 的工作细节,我们将保持简单,在预期输出中有 4 列。

正如我们前面看到的,在 RNN,一个隐藏层在展开时会连接到另一个隐藏层。假设一个隐藏层连接到下一个隐藏层,与前一个隐藏层和当前隐藏层之间的连接相关联的权重(whh)在维度上将是 3 × 3,因为 1 × 3 矩阵乘以 3 × 3 矩阵将产生 1 × 3 矩阵。下图中的最终隐藏层计算将在后续页面中解释。

A463052_1_En_10_Figc_HTML.jpg

注意,wxh 和 whh 是随机初始化,而隐藏层和最终隐藏层是计算出来的。在接下来的几页中,我们将看看这些计算是如何完成的。

隐藏层在不同时间步的计算如下:

{h}^{(t)}={\phi}_h\left({z}_h^{(t)}\right)={\phi}_h\left({W}_{xh}{x}^{(t)}+{W}_{hh}{h}^{\left(t-1\right)}\right)

其中A463052_1_En_10_Figd_HTML.gif是执行的激活(一般为 tanh 激活)。

从输入层到隐藏层的计算包括两个部分:

  • 输入层和 wxh 的矩阵乘法。
  • 隐藏层和 whh 的矩阵乘法。

给定时间步长的隐藏层值的最终计算将是前面两个矩阵乘法的总和,并将结果传递给双曲正切激活函数。

输入层和 wxh 的矩阵乘法如下所示:

A463052_1_En_10_Fige_HTML.jpg

以下部分介绍了不同时间步长下隐藏层值的计算。

时间步长 1

第一个时间步长的隐藏层值将是输入层和 wxh 之间的矩阵乘法的值(因为在前一个时间步长中没有隐藏层值):

A463052_1_En_10_Figf_HTML.jpg

时间步长 2

开始第二次输入,隐藏层由当前时间步的隐藏层分量和来自前一时间步的隐藏层分量组成:

A463052_1_En_10_Figh_HTML.jpg

A463052_1_En_10_Figg_HTML.jpg

时间步长 3

类似地,在第三时间步,输入将是当前时间步的输入和来自前一时间步的隐藏单位值。注意,前一时间步(t-1)中的隐藏单元也受到来自(t-2)的隐藏值的影响。

A463052_1_En_10_Figi_HTML.jpg

类似地,在第四个时间步计算隐藏层值。

现在我们已经计算出了隐藏层,我们让它通过一个激活,就像我们在传统 NN 中做的那样:

A463052_1_En_10_Figj_HTML.jpg

假设对于每个输入,隐藏层激活的输出大小为 1 × 3,为了获得大小为 1 × 4 的输出(因为预期输出“示例”的一次热编码版本的大小为 4 列),隐藏层的大小为什么应该为 3 × 4:

A463052_1_En_10_Figk_HTML.jpg

根据中间输出,我们执行 softmax 激活,如下所示:

A463052_1_En_10_Figl_HTML.jpg

softmax 的第二步是归一化每个像元值以获得概率值:

A463052_1_En_10_Figm_HTML.jpg

一旦获得概率,就可以通过计算预测和实际输出之间的交叉熵损失来计算损失。

最后,我们将以类似于 NN 的方式,通过前向和后向传播时期的组合来最小化损耗。

实现 RNN:简单

要了解 RNN 在 keras 中是如何实现的,我们先通过一个简单化的例子(只了解 RNN 的 keras 实现,然后通过在 Excel 中实现来巩固我们的理解):对两个句子进行分类(这两个句子有三个单词的穷举列表)。通过这个玩具示例,我们应该能够更好地快速理解输出(在 github 中代码为“simpleRNN.ipynb”):

from keras.preprocessing.text import one_hot
from keras.preprocessing.sequence import pad_sequences
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import Flatten
from keras.layers.recurrent import SimpleRNN
from keras.layers.embeddings import Embedding
from keras.layers import LSTM
import numpy as np

初始化文档并对与这些文档相对应的单词进行编码:

# define documents
docs = ['very good',
             'very bad']
# define class labels
labels = [1,0]
from collections import Counter
counts = Counter()
for i,review in enumerate(docs):
    counts.update(review.split())
words = sorted(counts, key=counts.get, reverse=True)
vocab_size=len(words)
word_to_int = {word: i for i, word in enumerate(words, 1)}
encoded_docs = []
for doc in docs:
    encoded_docs.append([word_to_int[word] for word in doc.split()])

将文档填充到两个单词的最大长度,这是为了保持一致性,以便所有输入都具有相同的大小:

A463052_1_En_10_Fign_HTML.jpg

# pad documents to a max length of 2 words
max_length = 2
padded_docs = pad_sequences(encoded_docs, maxlen=max_length, padding="pre")
print(padded_docs)

编译模型

SimpleRNN 函数的输入形状应为(时间步长数,每个时间步长的要素数)的形式。此外,一般来说,RNN 使用双曲正切作为激活函数。以下代码将输入形状指定为(2,1),因为每个输入都基于两个时间步长,并且每个时间步长只有一列表示它。unroll=True表示我们正在考虑以前的时间步骤:

# define the model
embed_length=1
max_length=2
model = Sequential()
model.add(SimpleRNN(1,activation='tanh', return_sequences=False,recurrent_initializer='Zeros',input_shape=(max_length,embed_length),unroll=True))
model.add(Dense(1, activation="sigmoid"))
# compile the model
model.compile(optimizer='adam', loss="binary_crossentropy", metrics=['acc'])
# summarize the model
print(model.summary())

SimpleRNN (1,)表示隐藏层中有一个神经元。return_sequences为假,因为我们没有返回任何输出序列,而且是单个输出:

A463052_1_En_10_Figo_HTML.jpg

模型编译完成后,让我们继续拟合模型,如下所示:

A463052_1_En_10_Figp_HTML.jpg

model.fit(padded_docs.reshape(2,2,1),np.array(labels).reshape(max_length,1),epochs=500)

注意我们已经重塑了padded_docs。这是因为我们需要在拟合时将训练数据集转换为如下格式:{数据大小,时间步长数,每个时间步长的要素}。此外,标签应该采用数组格式,因为编译模型中的最终密集层需要一个数组。

验证 RNN 的输出

现在我们已经安装好了玩具模型,让我们来验证之前创建的 Excel 计算。请注意,我们将输入作为原始编码{1,2,3 }—实际上,我们绝不会照原样采用原始编码,而是对输入进行一次热编码或创建嵌入。我们在本节中采用原始输入,只是为了比较 keras 的输出和我们将在 Excel 中进行的手工计算。

model.layers指定模型中的层,weights让我们了解与模型相关的层:

A463052_1_En_10_Figq_HTML.jpg

model.weights为我们提供了与模型中的权重相关的名称指示:

A463052_1_En_10_Figr_HTML.jpg

model.get_weights()给出了与模型相关的权重的实际值:

A463052_1_En_10_Figs_HTML.jpg

请注意,权重是有序的,即第一个权重值对应于kernel:0。换句话说,它与 wxh 相同,wxh 是与输入相关联的权重。

recurrent_kernel:0与 whh 相同,whh 是与先前的隐藏层和当前时间步的隐藏层之间的连接相关联的权重。bias:0是与输入相关的偏置。dense_2/kernel:0就是为什么——也就是连接隐藏层和输出的权重。dense_2/bias:0是与隐藏层和输出之间的连接相关的偏差。

让我们验证输入[1,3]的预测:

A463052_1_En_10_Figt_HTML.jpg

padded_docs[0].reshape(1,2,1)

A463052_1_En_10_Figu_HTML.jpg

import numpy as np
model.predict(padded_docs[0].reshape(1,2,1))

假设输入[1,3]的预测值为 0.53199(按此顺序),让我们在 Excel 中进行验证(在 github 中以“简单 RNN 工作验证. xlsx”的形式提供):

A463052_1_En_10_Figv_HTML.jpg

两个时间步长的输入值如下:

A463052_1_En_10_Figw_HTML.jpg

输入和权重之间的矩阵乘法计算如下:

A463052_1_En_10_Figx_HTML.jpg

现在矩阵乘法已经完成,我们将继续计算时间步长 0 中的隐藏层值:

A463052_1_En_10_Figy_HTML.jpg

时间步长 1 中的隐藏层值如下:

tanh(时间步长 1 中的隐藏层值×与隐藏层到隐藏层连接相关的权重(whh) +前一个隐藏层值)

让我们先计算一下双曲正切函数的内部:

A463052_1_En_10_Figz_HTML.jpg

现在我们将计算时间步骤 1 的最终隐藏层值:

A463052_1_En_10_Figaa_HTML.jpg

一旦计算出最终的隐藏层值,它将通过一个 sigmoid 层,因此最终的输出计算如下:

A463052_1_En_10_Figab_HTML.jpg

我们从 Excel 得到的最终输出与从 keras 得到的输出相同,因此是对我们之前看到的公式的验证:

A463052_1_En_10_Figac_HTML.jpg

实现 RNN:文本生成

现在我们已经看到了典型的 RNN 是如何工作的,让我们看看如何使用 keras 为 RNN 提供的 API(在 github 中以“RNN 文本生成. ipynb”的形式提供)来生成文本。

对于这个例子,我们将处理爱丽丝数据集( www.gutenberg.org/ebooks/11 ):

  1. 导入包:

    from keras.models import Sequential
    from keras.layers import Dense,Activation
    from keras.layers.recurrent import SimpleRNN
    import numpy as np
    
    
  2. 读取数据集:

    fin=open('/home/akishore/alice.txt',encoding='utf-8-sig')
    lines=[]
    for line in fin:
      line = line.strip().lower()
      line = line.decode("ascii","ignore")
      if(len(line)==0):
        continue
      lines.append(line)
    fin.close()
    text = " ".join(lines)
    
    
  3. Normalize the file to have only small case and remove punctuation, if any:

    A463052_1_En_10_Figad_HTML.jpg

    text[:100]
    
    
    # Remove punctuations in dataset
    import re
    text = text.lower()
    text = re.sub('[⁰-9a-zA-Z]+',' ',text)
    
    
  4. 对单词进行一次热编码:

    from collections import Counter
    counts = Counter()
    counts.update(text.split())
    words = sorted(counts, key=counts.get, reverse=True)
    chars = words
    total_chars = len(set(chars))
    nb_chars = len(text.split())
    char2index = {word: i for i, word in enumerate(chars)}
    index2char = {i: word for i, word in enumerate(chars)}
    
    
  5. Create the input and target datasets :

    A463052_1_En_10_Figae_HTML.jpg

    SEQLEN = 10
    STEP = 1
    input_chars = []
    label_chars = []
    text2=text.split()
    for i in range(0,nb_chars-SEQLEN,STEP):
        x=text2[i:(i+SEQLEN)]
        y=text2[i+SEQLEN]
        input_chars.append(x)
        label_chars.append(y)
    print(input_chars[0])
    print(label_chars[0])
    
    
  6. Encode the input and output datasets:

    A463052_1_En_10_Figaf_HTML.jpg

    X = np.zeros((len(input_chars), SEQLEN, total_chars), dtype=np.bool)
    y = np.zeros((len(input_chars), total_chars), dtype=np.bool)
    # Create encoded vectors for the input and output values
    for i, input_char in enumerate(input_chars):
        for j, ch in enumerate(input_char):
            X[i, j, char2index[ch]] = 1
        y[i,char2index[label_chars[i]]]=1
    print(X.shape)
    print(y.shape)
    
    

    Note that, the shape of X indicates that we have a total 30,407 rows that have 10 words each, where each of the 10 words is expressed in a 3,028-dimensional space (since there are a total of 3,028 unique words).  

  7. Build the model:

    A463052_1_En_10_Figag_HTML.jpg

    HIDDEN_SIZE = 128
    BATCH_SIZE = 128
    NUM_ITERATIONS = 100
    NUM_EPOCHS_PER_ITERATION = 1
    NUM_PREDS_PER_EPOCH = 100
    model = Sequential()
    model.add(SimpleRNN(HIDDEN_SIZE,return_sequences=False,input_shape=(SEQLEN,total_chars),unroll=True))
    model.add(Dense(nb_chars, activation="sigmoid"))
    model.compile(optimizer='rmsprop', loss="categorical_crossentropy")
    model.summary()
    
    
  8. 运行该模型,我们随机生成一个种子文本,并尝试根据给定的种子单词集预测下一个单词:

    for iteration in range(150):
        print("=" * 50)
        print("Iteration #: %d" % (iteration))
        # Fitting the values
        model.fit(X, y, batch_size=BATCH_SIZE, epochs=NUM_EPOCHS_PER_ITERATION)
    
        # Time to see how our predictions fare
        # We are creating a test set from a random location in our dataset
        # In the code below, we are selecting a random input as our seed value of words
        test_idx = np.random.randint(len(input_chars))
        test_chars = input_chars[test_idx]
        print("Generating from seed: %s" % (test_chars))
        print(test_chars)
        # From the seed words, we are tasked to predict the next words
        # In the code below, we are predicting the next 100 words (NUM_PREDS_PER_EPOCH) after the seed words
        for i in range(NUM_PREDS_PER_EPOCH):
            # Pre processing the input data, just like the way we did before training the model
            Xtest = np.zeros((1, SEQLEN, total_chars))
            for i, ch in enumerate(test_chars):
                Xtest[0, i, char2index[ch]] = 1
            # Predict the next word
            pred = model.predict(Xtest, verbose=0)[0]
            # Given that, the predictions are probability values, we take the argmax to fetch the location of highest probability
            # Extract the word belonging to argmax
            ypred = index2char[np.argmax(pred)]
            print(ypred,end=' ')
            # move forward with test_chars + ypred so that we use the original 9 words + prediction for the next prediction
            test_chars = test_chars[1:] + [ypred]
    
    

初始迭代中的输出只是一个词——always!

150 次迭代结束时的输出如下(注意,下面只是部分输出):

A463052_1_En_10_Figah_HTML.jpg

前面的输出几乎没有损失。如果您在执行代码后仔细查看输出,经过一些迭代后,它会重新生成数据集中存在的精确文本—这是一个潜在的过拟合问题。另外,请注意我们输入的形状:大约 30K 个输入,其中有 3028 列。鉴于行与列的比率较低,有可能会过度匹配。随着输入样本数量的增加,这种方法可能会更有效。

拥有大量列的问题可以通过使用嵌入来解决,这与我们计算单词向量的方式非常相似。本质上,嵌入表示一个低得多的维度空间中的单词。

RNN 的嵌入层

为了了解嵌入是如何工作的,让我们来看一个数据集,该数据集试图根据客户推文预测航空公司的客户情绪(代码在 github 中为“RNNsentiment.ipynb”):

  1. As always, import the relevant packages :

    A463052_1_En_10_Figai_HTML.jpg

     #import relevant packages
    from keras.layers import Dense, Activation
    from keras.layers.recurrent import SimpleRNN
    from keras.models import Sequential
    from keras.utils import to_categorical
    from keras.layers.embeddings import Embedding
    from sklearn.cross_validation import train_test_split
    import numpy as np
    import nltk
    from nltk.corpus import stopwords
    import re
    import pandas as pd
    #Let us go ahead and read the dataset:
    t=pd.read_csv('/home/akishore/airline_sentiment.csv')
    t.head()
    
    
    import numpy as np
    t['sentiment']=np.where(t['airline_sentiment']=="positive",1,0)
    
    
  2. 鉴于文本有噪声,我们将通过删除标点符号并将所有单词转换成小写字母来对其进行预处理:

     def preprocess(text):
        text=text.lower()
        text=re.sub('[⁰-9a-zA-Z]+',' ',text)
        words = text.split()
        #words2=[w for w in words if (w not in stop)]
        #words3=[ps.stem(w) for w in words]
        words4=' '.join(words)
        return(words4)
    t['text'] = t['text'].apply(preprocess)
    
    
  3. Similar to how we developed in the previous section, we convert each word into an index value as follows:

    A463052_1_En_10_Figaj_HTML.jpg

    from collections import Counter
    counts = Counter()
    for i,review in enumerate(t['text']):
        counts.update(review.split())
    words = sorted(counts, key=counts.get, reverse=True)
    words[:10]
    
    
    chars = words
    nb_chars = len(words)
    word_to_int = {word: i for i, word in enumerate(words, 1)}
    int_to_word = {i: word for i, word in enumerate(words, 1)}
    word_to_int['the']
    #3
    int_to_word[3]
    #the
    
    
  4. Map each word in a review to its corresponding index :

    A463052_1_En_10_Figak_HTML.jpg

     mapped_reviews = []
    for review in t['text']:
        mapped_reviews.append([word_to_int[word] for word in review.split()])
    t.loc[0:1]['text']
    
    

    A463052_1_En_10_Figal_HTML.jpg

    mapped_reviews[0:2]
    
    

    Note that, the index of virginamerica is the same in both reviews (104).  

  5. 初始化长度为 200 的零序列。请注意,我们选择了 200 作为序列长度,因为没有评论超过 200 个单词。此外,以下代码的第二部分确保对于所有小于 200 个单词的评论,所有开始的索引都被零填充,只有最后的索引被填充了与评论中出现的单词相对应的索引:

     sequence_length = 200
    sequences = np.zeros((len(mapped_reviews), sequence_length),dtype=int)
    for i, row in enumerate(mapped_reviews):
        review_arr = np.array(row)
        sequences[i, -len(row):] = review_arr[-sequence_length:]
    
    
  6. 我们进一步将数据集分为训练和测试数据集,如下:

     y=t['sentiment'].values
    X_train, X_test, y_train, y_test = train_test_split(sequences, y, test_size=0.30,random_state=10)
    y_train2 = to_categorical(y_train)
    y_test2 = to_categorical(y_test)
    
    
  7. Once the datasets are in place, we go ahead and create our model, as follows. Note that embedding as a function takes in as input the total number of unique words, the reduced dimension in which we express a given word, and the number of words in an input:

    A463052_1_En_10_Figam_HTML.jpg

     top_words=12679
    embedding_vecor_length=32
    max_review_length=200
    model = Sequential()
    model.add(Embedding(top_words, embedding_vecor_length, input_length=max_review_length))
    model.add(SimpleRNN(1, return_sequences=False,unroll=True))
    model.add(Dense(2, activation="softmax"))
    model.compile(loss='categorical_crossentropy', optimizer="adam", metrics=['accuracy'])
    print(model.summary())
    model.fit(X_train, y_train2, validation_data=(X_test, y_test2), epochs=50, batch_size=1024)
    
    

现在让我们看看前面模型的汇总输出。数据集中总共有 12,679 个唯一单词。嵌入层确保我们在 32 维空间中表示每个单词,因此在嵌入层中有 405,728 个参数。

现在我们有 32 个嵌入的维度输入,每个输入现在都连接到一个隐藏层单元——因此有 32 个权重。加上这 32 个砝码,我们就会有偏差。对应于该层的最终权重将是连接先前的隐藏单位值和当前隐藏单位的权重。因此总共有 34 个砝码。

注意,假设有一个来自嵌入层的输出,我们不需要在 SimpleRNN 层中指定输入形状。一旦模型运行,输出分类准确率接近 87%。

传统 RNN 的问题

图 10-5 显示了一个传统的 RNN,它考虑了多个时间步长来进行预测。

A463052_1_En_10_Fig5_HTML.png

图 10-5

An RNN with multiple time steps

请注意,随着时间步长的增加,来自更早层的输入对后面层的输出的影响会小得多。这可以从以下内容中看出(现在,我们将忽略偏差项):

  • h 1 = Wx 1
  • h2= Wx2+Uh1= Wx2+UWx1
  • h3= Wx3+Uh2= Wx3+UWx2+U2Wx1
  • h4= Wx4+Uh3= Wx4+UWX3+U2WX2+U3WX1
  • h5= Wx5+Uh4= Wx5+UWX4+U2WX3+U3WX2+U4WX1

注意,随着时间戳的增加,如果 U > 1,隐藏层的值高度依赖于 X 1 ,如果 U < 1,则稍微依赖于 X 1

消失梯度问题

U 4 相对于 U 的斜率为 4 × U 3 。在这种情况下,请注意,如果 U <为 1,则梯度非常小,因此,如果在更晚的时间步长的输出取决于给定时间步长的输入,则达到理想权重需要很长时间。这导致了一个问题,即在一些句子中,对时间步长中出现得更早的单词存在依赖性。比如“我来自印度。我说一口流利的 ____”在这种情况下,如果我们不考虑第一句话,第二句话的输出“我说流利的 ____”可能是任何语言的名称。因为我们在第一句中提到了国家,我们应该能够将事情缩小到特定于印度的语言。

爆炸梯度的问题

在前面的场景中,如果 U > 1,则梯度增加的量要大得多。这将导致在时间步长中较早出现的输入具有非常高的权重,而在我们试图预测的单词附近出现的输入具有低的权重。

因此,根据 U(隐藏层的权重)的值,权重要么更新得非常快,要么需要很长时间。

鉴于消失/爆炸梯度是一个问题,我们应该以稍微不同的方式处理 rnn。

-什么

长短期记忆(LSTM)是一种架构,有助于克服我们之前看到的消失或爆炸梯度问题。在这一节,我们将看看 LSTM 的建筑,看看它如何帮助克服传统 RNN 的问题。

LSTM 如图 10-6 所示。

A463052_1_En_10_Fig6_HTML.png

图 10-6

LSTM

注意,尽管隐藏层的输入 X 和输出(h)保持不变,但是隐藏层内发生的激活是不同的。不像传统的 RNN,有 tanh 激活,在 LSTM 有不同的激活发生。我们将逐一介绍。

在图 10-7 中,X 和 h 代表输入和隐藏层,正如我们前面看到的。

A463052_1_En_10_Fig7_HTML.png

图 10-7

Various components of LSTM

c 表示单元状态。您可以将单元格状态视为获取长期依赖关系的一种方式。

f 代表了忘门:

{f}_t=\sigma \left({W}_{xf}{x}^{(t)}+{W}_{hf}{h}^{\left(t-1\right)}+{b}_f\right)

请注意,乙状结肠给了我们一种机制来指定需要忘记什么。这样 h(t–1)中捕捉到的一些历史词汇就被选择性遗忘了。

一旦我们发现需要忘记什么,单元格状态就会更新如下:

{c}_t=\left({c}_{t-1}\otimes f\right)

注意,A463052_1_En_10_Figan_HTML.jpg代表元素间的乘法。

想象一下,一旦我们填写了“我住在印度”中的空白。我说 ____”有了一个印度语的名字,我们就不再需要“我住在印度”的语境了。这就是“遗忘之门”有助于有选择地遗忘不再需要的信息的地方。

一旦我们弄清楚了单元格状态中需要忘记什么,我们就可以根据当前输入更新单元格状态。

在下一步中,需要更新单元状态的输入通过输入之上的 sigmoid 应用程序实现,更新幅度(正或负)通过 tanh 激活获得。

输入可以指定如下:

{i}_t=\sigma \left({W}_{xi}{x}^{(t)}+{W}_{hi}{h}^{\left(t-1\right)}+{b}_i\right)

调制可以这样指定:

{g}_t=\tanh \left({W}_{xg}{x}^{(t)}+{W}_{hg}{h}^{\left(t-1\right)}+{b}_g\right)

因此,单元状态最终被更新如下:

{C}^{(t)}=\left({C}^{\left(t-1\right)}\odot {f}_t\right)\oplus \left({i}_t\odot {g}_t\right)

在最终的 gate 中,我们需要指定输入和单元格状态组合的哪一部分需要输出到下一个隐藏层:

{o}_t=\sigma \left({W}_{xo}{x}^{(t)}+{W}_{ho}{h}^{\left(t-1\right)}+{b}_o\right)

最后隐藏的图层是这样表示的:

{h}^{(t)}={o}_t\odot \tanh \left({C}^{(t)}\right)

鉴于细胞状态可以记住稍后需要的值,LSTM 在预测下一个单词方面提供了比传统 RNN 更好的结果,通常是在情感分类方面。这在有长期依赖关系需要处理的场景中特别有用。

在 keras 实现基本 LSTM

为了了解到目前为止提出的理论如何转化为行动,让我们重新看看我们之前看到的玩具示例(在 github 中代码为“LSTM 玩具示例. ipynb”):

  1. 导入相关包:

    from keras.preprocessing.text import one_hot
    from keras.preprocessing.sequence import pad_sequences
    from keras.models import Sequential
    from keras.layers import Dense
    from keras.layers import Flatten
    from keras.layers.recurrent import SimpleRNN
    from keras.layers.embeddings import Embedding
    from keras.layers import LSTM
    import numpy as np
    
    
  2. 定义文件和标签:

    # define documents
    docs = ['very good',
                 'very bad']
    # define class labels
    labels = [1,0]
    
    
  3. One-hot-encode the documents:

    A463052_1_En_10_Figao_HTML.jpg

    from collections import Counter
    counts = Counter()
    for i,review in enumerate(docs):
        counts.update(review.split())
    words = sorted(counts, key=counts.get, reverse=True)
    vocab_size=len(words)
    word_to_int = {word: i for i, word in enumerate(words, 1)}
    encoded_docs = []
    for doc in docs:
        encoded_docs.append([word_to_int[word] for word in doc.split()])
    encoded_docs
    
    
  4. Pad documents to a maximum length of two words :

    A463052_1_En_10_Figap_HTML.jpg

    max_length = 2
    padded_docs = pad_sequences(encoded_docs, maxlen=max_length, padding="pre")
    print(padded_docs)
    
    
  5. Build the model :

    A463052_1_En_10_Figaq_HTML.jpg

    model = Sequential()
    model.add(LSTM(1,activation='tanh', return_sequences=False,recurrent_initializer='Zeros',recurrent_activation='sigmoid',
                   input_shape=(2,1),unroll=True))
    model.add(Dense(1, activation="sigmoid"))
    model.compile(optimizer='adam', loss="binary_crossentropy", metrics=['acc'])
    print(model.summary())
    
    

请注意,在前面的代码中,我们将递归初始化器和递归激活初始化为某些值,只是为了让这个玩具示例在 Excel 中实现时更容易理解。目的是帮助您理解后端发生了什么。

一旦模型如所讨论的那样被初始化,让我们继续拟合模型:

A463052_1_En_10_Figar_HTML.jpg

model.fit(padded_docs.reshape(2,2,1),np.array(labels).reshape(max_length,1),epochs=500)

该模型的各层如下。这里是model.layers:

A463052_1_En_10_Figas_HTML.jpg

权重和权重的顺序可以如下获得:

A463052_1_En_10_Figaw_HTML.jpg

model.layers[1].trainable_weights

A463052_1_En_10_Figav_HTML.jpg

model.layers[1].get_weights()

A463052_1_En_10_Figau_HTML.jpg

model.layers[0].trainable_weights

A463052_1_En_10_Figat_HTML.jpg

model.layers[0].get_weights()

从前面的代码中,我们可以看到,首先获得输入的权重(kernel),然后是对应于隐藏层的权重(recurrent_kernel),最后是 LSTM 层中的偏差。

同样,在dense层(连接隐藏层和输出的层)中,要与隐藏层相乘的权重排在第一位,其次是偏差。

另请注意,权重和偏差在 LSTM 图层中出现的顺序如下:

  1. 输入门
  2. 忘记大门
  3. 调制门(单元门)
  4. 输出门

现在我们有了输出,让我们继续计算输入的预测。注意,就像上一节一样,我们使用原始编码输入(1,2,3)而不对它们进行进一步处理,只是为了看看计算是如何进行的。

在实践中,我们将进一步处理输入,可能将它们编码成向量以获得预测,但在本例中,我们感兴趣的是通过在 Excel 中复制 LSTM 的预测来巩固我们对 LSTM 如何运作的知识:

A463052_1_En_10_Figax_HTML.jpg

padded_docs[1].reshape(1,2,1)

A463052_1_En_10_Figay_HTML.jpg

model.predict(padded_docs[1].reshape(1,2,1))

现在,我们已经从模型中获得了 0.4485 的预测概率,让我们在 Excel(github 中以“LSTM 工作细节. xlsx”的名称提供)中手工计算这些值:

A463052_1_En_10_Figaz_HTML.jpg

注意,这里的值取自 keras 的model.layers[0].get_weights()输出。

在继续计算各个门的值之前,请注意,我们已经将 recurrent layer (h t-1 )的值初始化为 0。在第一个时间步长中,输入值为 1。让我们计算一下各个关口的价值:

A463052_1_En_10_Figba_HTML.jpg

获得上述输出的计算如下:

A463052_1_En_10_Figbb_HTML.jpg

现在已经计算了各个门的所有值,我们将计算输出(隐藏层):

A463052_1_En_10_Figbc_HTML.jpg

刚刚显示的隐藏层值是输入为 1 的时间步长的隐藏层输出。

现在,我们将继续计算输入为 2 时的隐藏层值(这是我们之前在代码中预测的数据点的第二个时间步长的输入):

A463052_1_En_10_Figbd_HTML.jpg

让我们看看如何获得第二个输入的各种门和隐藏层的值。这里需要注意的关键是,第一个时间步长输出的隐藏层是第二个输入中所有门的计算输入:

A463052_1_En_10_Figbe_HTML.jpg

最后,假设我们已经计算了第二时间步的隐藏层输出,我们计算输出如下:

A463052_1_En_10_Figbf_HTML.jpg

前面计算的最终输出如下所示:

A463052_1_En_10_Figbg_HTML.jpg

请注意,我们得到的输出与我们在 keras 输出中看到的相同。

实现用于情感分类的 LSTM

在最后一节中,我们使用 keras 中的 RNN 实现了情感分类。在这一节中,我们将看看如何使用 LSTM 来实现这一点。我们在上面看到的代码中唯一的变化将是模型编译部分,我们将使用 LSTM 代替SimpleRNN——其他一切都将保持不变(代码可在 github 的“RNN 情绪. ipynb”文件中获得):

top_words=nb_chars
embedding_vecor_length=32
max_review_length=200
model = Sequential()
model.add(Embedding(top_words, embedding_vecor_length, input_length=max_review_length))
model.add(LSTM(10))
model.add(Dense(2, activation="softmax"))
model.compile(loss='categorical_crossentropy', optimizer="adam", metrics=['accuracy'])
print(model.summary())
model.fit(X_train, y_train2, validation_data=(X_test, y_test2), epochs=50, batch_size=1024)

一旦你实现了这个模型,你会发现 LSTM 的预测精度比 RNN 的略高。实际上,对于我们之前看到的数据集,LSTM 给出了 91%的准确率,而 RNN 给出了 87%的准确率。这可以通过调整函数提供的各种超参数来进一步微调。

在 R 中实现 RNN

为了了解如何在 R 中实现 RNN/LSTM,我们将使用随kerasR包一起预构建的 IMDB 情感分类数据集(在 github 中代码为“kerasR_code_RNN.r”):

# Load the dataset
library(kerasR)
imdb <- load_imdb(num_words = 500, maxlen = 100)

注意,我们通过将num_words指定为一个参数,只获取前 500 个单词。我们也只获取那些长度不超过 100 字的 IMDB 评论。

让我们探索一下数据集的结构:

str(imdb)

我们应该注意到,在随kerasR包一起提供的预构建的 IMDB 数据集中,每个单词都被它默认表示的索引所替换。因此我们不必执行单词到索引的映射步骤:

# Build the model with an LSTM
model <- Sequential()

model$add(Embedding(500, 32, input_length = 100, input_shape = c(100)))

model$add(LSTM(32)) # Use SimpleRNN, if we were to perform a RNN function
model$add(Dense(256))
model$add(Activation('relu'))
model$add(Dense(1))
model$add(Activation('sigmoid'))
# Compile and fit the model
keras_compile(model,  loss = 'binary_crossentropy', optimizer = Adam(),metrics='binary_accuracy')
keras_fit(model, X_train, Y_train, batch_size = 1024, epochs = 50, verbose = 1,validation_data = list(X_test,Y_test))

上述结果使测试数据集预测的准确率接近 79%。

摘要

在本章中,您学习了以下内容:

  • rnn 在处理具有时间相关性的数据时非常有用。
  • 在处理数据的长期依赖性时,rnn 面临梯度消失或爆炸的问题。
  • 在这种情况下,LSTM 和其他最新的建筑就派上了用场。
  • LSTM 的工作原理是将信息存储在单元状态中,忘记不再有帮助的信息,根据当前输入选择信息以及需要添加到单元状态的信息量,最后选择需要输出到下一个状态的信息。

十一、聚类

聚类的字典含义是分组。在数据科学中,聚类也是一种无监督的学习技术,有助于对我们的数据点进行分组。

对数据点(行)进行分组的好处包括:

  • 为了让企业用户了解客户中的各种类型的用户
  • 在集群(集团)层面而非整体层面做出商业决策
  • 有助于提高预测的准确性,因为不同的群体表现出不同的行为,因此可以为每个群体建立单独的模型

在本章中,您将学习以下内容:

  • 不同类型的聚类
  • 不同类型的集群如何工作
  • 集群的使用案例

聚类直觉

让我们考虑一个有 4,000 个销售点的零售店的例子。中央规划团队必须对所有门店的店长进行年终评估。评估商店经理的主要指标是商店一年的总销售额。

  • 场景 1:商店经理仅根据销售额进行评估。我们将根据商店的销售额对所有商店经理进行排名,销售额最高的商店经理将获得最高奖励。缺点:这种方法的主要缺点是,我们没有考虑到一些商店位于城市,与农村商店相比,城市的销售额通常很高。城市商店销售额高的最大原因可能是人口多和/或城市消费者的购买力更高。
  • 场景 2:如果我们可以将商店分为城市和农村,或者高购买力客户经常光顾的商店,或者特定人群(比如年轻家庭)经常光顾的商店,并将每个商店称为一个集群,那么只有属于同一个集群的商店经理可以进行比较。例如,如果我们将所有商店分为城市商店和农村商店,那么城市商店的所有商店经理都可以相互比较,农村商店经理也是如此。缺点:虽然我们可以更好地比较不同商店经理的表现,但商店经理仍然会受到不公平的比较。例如,两个城市商店可能不同,一个商店位于上班族经常光顾的中央商务区,另一个位于城市的住宅区。很有可能中央商业区的商店在城市商店中有更高的销售额。

尽管场景 2 仍然有一个缺点,但它并不像场景 1 那样糟糕。因此,将商店分为两类有助于更准确地衡量商店经理的表现。

构建商店集群以进行性能比较

在场景 2 中,我们看到商店经理仍然可以质疑比较过程不公平——因为商店仍然可以在一个或多个参数上有所不同。

商店经理可能会列举出他们的商店与其他商店不同的多种原因:

  • 不同商店出售的产品的差异
  • 光顾商店的顾客年龄组的差异
  • 光顾商店的顾客生活方式的差异

现在,为了简单起见,让我们定义我们所有的组:

  • 城市商店对农村商店
  • 高端产品商店与低端产品商店
  • 高年龄组客户商店与低年龄组客户商店的对比
  • 高端购物者商店与经济型购物者商店

我们可以仅基于列出的因素创建总共 16 个不同的聚类(组)(所有组合的详尽列表产生 16 个组)。

我们讨论了区分商店的四个重要因素,但仍然有许多其他因素可以区分商店。例如,位于该州多雨地区的商店与位于阳光充足地区的商店相比。

本质上,商店在多个维度/因素上彼此不同。但由于某些因素,商店经理的结果可能会有很大差异,而其他因素的影响可能很小。

理想聚类

到目前为止,我们看到每个商店都是独一无二的,可能会有这样一种情况,商店经理总能举出一个或另一个原因来说明为什么他们不能与同一个集群中的其他商店进行比较。例如,商店经理可以说,虽然属于该组的所有商店都是城市商店,大多数高端客户都在中年类别,但他们的商店表现不如其他商店,因为它靠近一直在大力促销的竞争对手商店,因此,与同一组的其他商店相比,该商店的销售额没有那么高。

因此,如果我们把所有的原因都考虑进去,我们最终可能会得到一个细粒度的分段,以至于每个集群中只有一个商店。这将产生一个很好的聚类输出,其中每个商店都是不同的,但是这个结果是没有用的,因为现在我们不能跨商店进行比较。

在没有聚类和太多聚类之间取得平衡:K-means 聚类

我们已经看到,有多少个商店就有多少个分类,这是一个区分每个商店的很好的分类,但这是一个无用的分类,因为我们无法从中得出任何有意义的结果。同时,没有集群也是不好的,因为商店经理可能会与完全不同的商店的商店经理进行不准确的比较。

因此,使用聚类过程,我们努力达到平衡。我们希望尽可能多地确定区分商店的几个因素,只考虑那些用于评估的因素,而忽略其他因素。使用 k-means 聚类可以尽可能多地识别商店之间的少数差异因素。

为了了解 k-means 聚类是如何工作的,让我们考虑一个场景:您是一家披萨连锁店的老板。你有预算在一个社区开三家新店。你如何想出开设三个分店的最佳地点?

现在,我们假设所有道路上的交通流量都是相同的。假设我们的邻居看起来像图 11-1 。

A463052_1_En_11_Fig1_HTML.jpg

图 11-1

Each marker represents a household

理想情况下,我们会想出三个插座,它们彼此相距很远,但总的来说离大多数邻居最近。比如类似图 11-2 的东西。

A463052_1_En_11_Fig2_HTML.jpg

图 11-2

The circles represent potential outlet locations

这看起来不错,但是我们能找到更好的出口位置吗?让我们做一个练习。我们将尝试执行以下操作:

  1. 尽量缩短每家每户到最近的披萨店的距离
  2. 最大化每个披萨店之间的距离

假设一家披萨店只能给两家外卖。如果两家的需求是一致的,那么比萨饼店是正好位于两家之间,还是靠近一家或另一家更好呢?如果送货到房屋 A 需要 15 分钟,送货到房屋 B 需要 45 分钟,凭直觉,我们似乎最好将配送中心设在送货时间为 30 分钟的地方,也就是说,正好在两个房屋之间。如果不是这样,经销店可能经常无法兑现其在 45 分钟内为 B 家送货的承诺,但却永远无法兑现其对 a 家的承诺。

聚类的过程

在目前的情况下,我们如何想出一个更科学的方法来确定正确的比萨饼外卖店?该过程或算法如下:

  1. Randomly come up with three places where we can start outlets, as shown in Figure 11-3.

    A463052_1_En_11_Fig3_HTML.jpg

    图 11-3

    Random locations  

  2. Measure the distance of each house to the three outlet locations. The outlet that is closest to the household delivers to the household. The scenario would look like Figure 11-4.

    A463052_1_En_11_Fig4_HTML.jpg

    图 11-4

    Better informed locations  

  3. As we saw earlier, we are better off if the delivery outlet is in the middle of households than far away from the majority of the households. Thus, let’s change our earlier planned outlet location to be in the middle of the households (Figure 11-5).

    A463052_1_En_11_Fig5_HTML.jpg

    图 11-5

    Locations in the middle  

  4. We see that the delivery outlet locations have changed to be more in the middle of each group. But because of the change in location, there might be some households that are now closer to a different outlet. Let’s reassign households to stores based on their distance to different stores (Figure 11-6).

    A463052_1_En_11_Fig6_HTML.jpg

    图 11-6

    Reassigning households  

  5. Now that some households (comparing Figures 11-5 and 11-6) have a different outlet that serves them, let’s recompute the middle point of that group of households (Figure 11-7).

    A463052_1_En_11_Fig7_HTML.jpg

    图 11-7

    Recomputing the middles  

  6. 既然聚类中心已经改变,现在又有一个机会,家庭需要重新分配到一个不同的出路,而不是目前的出路。

这些步骤继续进行,直到不再将家庭重新分配到不同的群,或者达到最大的特定迭代次数。

正如你所看到的,事实上我们可以想出一个更科学的/分析性的方法来找到可以打开出口的最佳位置。

K 均值聚类算法的工作细节

我们将开设三家分店,所以我们想出了三组家庭,每组由不同的分店提供服务。

k-means 聚类中的 k 代表我们将在现有数据集中创建的组的数量。在我们在上一节中经历的算法的一些步骤中,一旦一些家庭从一个组改变到另一个组,我们就不断更新中心。我们更新中心的方法是取所有数据点的平均值,因此是 k-means。

最后,在经历了所描述的步骤之后,我们从原始数据集中获得了三组三个数据点或三个聚类。

对数据集应用 K-means 算法

让我们看看如何在数据集上实现 k-means 聚类(在 github 中以“clustering process.xlsx”的形式提供),如下所示:

| X | Y | 串 | | :-- | :-- | :-- | | five | Zero | one | | five | Two | Two | | three | one | one | | Zero | four | Two | | Two | one | one | | four | Two | Two | | Two | Two | one | | Two | three | Two | | one | three | one | | five | four | Two |

假设 X 和 Y 是我们希望聚类所基于的独立变量。假设我们想将这个数据集分成两个集群。

第一步,我们随机初始化聚类。因此,上表中的集群列是随机初始化的。

让我们计算每个集群的质心:

| 图心 | one | Two | | :-- | :-- | :-- | | X | Two point six | Three point two | | Y | One point four | three |

注意,值 2.6 是属于聚类 1 的所有 X 值的平均值。类似地,计算其他中心。

现在让我们计算每个点到两个聚类中心的距离。离数据点最近的聚类中心是数据点应该属于的聚类:

A463052_1_En_11_Figa_HTML.jpg

在 L & M 列中,我们计算了每个点到两个星团中心的距离。通过查看与数据点具有最小距离的聚类来更新列 O 中的聚类中心。

注意到一个数据点的聚类中有一个变化,我们再次继续前面的步骤,但是现在使用更新的中心。对于我们所做的两次迭代,总体计算如下所示:

A463052_1_En_11_Figb_HTML.jpg

我们不断重复这个过程,直到数据点所属的聚类不再发生变化。如果一个数据点簇持续变化,我们可能会在几次迭代后停止。

K-均值聚类算法的性质

如前所述,聚类练习的目标是以满足以下条件的方式创建不同的组:

  1. 属于同一组的所有点尽可能彼此靠近
  2. 每个组的中心尽可能远离另一个组的中心

有一些方法可以帮助评估基于这些目标的聚类输出的质量。

让我们通过一个样本数据集(在 github 中以“clustering output interpretation . xlsx”的名称提供)来巩固我们对这些属性的理解。假设我们有一个数据集如下:两个独立变量(x 和 y)和它们所属的对应聚类(一共四个聚类,只是这个例子):

A463052_1_En_11_Figc_HTML.jpg

四个聚类中心如下:

A463052_1_En_11_Figd_HTML.png

我们计算每个点相对于其对应的聚类中心的距离如下:

A463052_1_En_11_Fige_HTML.jpg

注意 withinss 中的列计算每个点到其对应的聚类中心的距离。让我们来看看用来得出上述结果的公式:

A463052_1_En_11_Figg_HTML.jpg

A463052_1_En_11_Figf_HTML.jpg

总平方和

在原始数据集本身被视为聚类的情况下,原始数据集的中点被视为中心。Totss 是所有点到数据集中心的距离的平方之和。

让我们看看公式:

A463052_1_En_11_Figh_HTML.jpg

总平方和是从单元格 B15 到单元格 C24 的所有值的总和。

聚类中心

每个聚类的聚类中心将是落在同一聚类中的所有点的中点(平均值)。例如,在 Excel 工作表中,聚类中心是在列 I 和 j 中计算的,请注意,这只是属于同一聚类的所有点的平均值。

-再见-再见

Tot.withinss 是所有点到其相应聚类中心的距离平方的总和。

在...之间

betwess 是 totss 和 tot.withinss 的区别。

在 R 中实现 K-means 聚类

R 中的 K-means 聚类是通过使用kmeans函数实现的,如下所示(作为“clustering_code”提供。github 中的 r”)。

# Lets generate dataset randomly
x=runif(1000)
y=runif(1000)

data=cbind(x,y)
# One would have to specify the dataset along with the number of clusters in input
km=kmeans(data,2)

km的输出是前面讨论的主要指标:

A463052_1_En_11_Figi_HTML.jpg

在 Python 中实现 K-means 聚类

Python 中的 K-means 集群是通过scikit-learn库中可用的函数实现的,如下所示(在 github 中可用为“clustering.ipynb”):

# import packages and dataset
import pandas as pd
import numpy as np
data2=pd.read_csv('D:/data.csv')
# fit k-means with 2 clusters
from sklearn.cluster import KMeans
kmeans = KMeans(n_clusters=2)
kmeans.fit(data2)

在这段代码中,我们从名为data2的原始数据集中提取了两个集群。可以通过指定kmeans.labels_来提取每个数据点所属聚类的结果标签。

主要指标的重要性

如前所述,执行聚类的目的是将所有彼此非常接近的数据点归入一个组,并使这些组彼此尽可能远离。

这是另一种说法:

  1. 最小化簇内距离
  2. 最大化集群间距离

让我们看看前面讨论的指标如何帮助实现目标。当没有聚类时(即所有数据集被视为一个聚类),每个点到聚类中心(有一个聚类的地方)的总距离是 totss。当我们在数据集中引入聚类时,聚类内每个点到相应聚类中心的距离之和是 tot.withinss。注意,随着聚类数量的增加,tot.withinss 不断减少。

考虑一种情况,其中聚类的数量等于数据点的数量。在那种情况下 Tot.withinss 等于 0,因为每个点到聚类中心(也就是点本身)的距离是 0。

因此,tot.withinss 是一个衡量簇内距离的指标。tot . withinss/tots 的比率越低,聚类过程的质量越高。

但是我们也需要注意,tot.withinss = 0 的场景就是聚类变得无用的场景,因为每个点本身就是一个聚类。

在下一节中,我们将以稍微不同的方式使用指标 tot.withinss / totss。

确定最佳 K

我们尚未回答的一个主要问题是如何获得最佳 k 值?换句话说,数据集内聚类的最佳数量是多少?

为了回答这个问题,我们将使用上一节中使用的度量标准:tot.withinss / totss 的比率。要查看当我们改变集群数量(k)时度量如何变化,请参见以下代码:

A463052_1_En_11_Figj_HTML.jpg

我们正在用 10,000 个随机初始化的 x 和 y 值创建一个数据集data

现在,当我们改变 k 值时,我们将探究度量的值。该图如图 11-8 所示。

A463052_1_En_11_Fig8_HTML.jpg

图 11-8

Variation in tot.withinss/totss over different values of k

注意,随着 k 值从 k = 1 增加到 k = 2,度量急剧减小,类似地,当 k 从 2 减少到 4 时,度量减小。

然而,随着 k 的进一步降低,度量的值不会降低很多。因此,谨慎的做法是将 k 值保持在接近 7 的水平,因为在这一点上获得了最大的下降,度量(tot.withinss / totss)的任何进一步下降都与 k 的增加没有很好的相关性。

鉴于曲线看起来像一个肘部,它有时被称为肘部曲线。

自顶向下与自底向上聚类

到目前为止,在 k-means 聚类的过程中,我们不知道最佳的聚类数目,所以我们不断尝试具有多个 k 值的各种场景。这是自底向上方法的相对较小的问题之一,其中我们从假设没有聚类开始,慢慢地不断建立多个聚类,一次一个,直到我们根据肘形曲线找到最佳 k。

自顶向下聚类从另一个角度看待同一过程。它假设每个点本身就是一个聚类,并尝试根据点与其他点的距离来组合点。

分层聚类

分层聚类是自顶向下聚类的经典形式。在此过程中,计算每个点到其余点的距离。一旦计算出距离,最接近所考虑的点的点被组合以形成聚类。这个过程在所有点上重复,从而获得最终的聚类。

层次部分来源于我们从一个点开始,与另一个点结合,然后将这个点的结合与第三个点结合,不断重复这个过程。

让我们通过一个例子来看看如何实现层次聚类。假设我们有六个不同的数据点——A、B、C、D、E、f。不同数据点相对于其他点的欧几里德距离如图 11-9 所示。

A463052_1_En_11_Fig9_HTML.png

图 11-9

Distance of data points

我们看到最小距离在 D 和 f 之间。因此,我们将 D 和 f 组合在一起。得到的矩阵现在看起来如图 11-10 所示。

A463052_1_En_11_Fig10_HTML.png

图 11-10

The resulting matrix

我们如何填充图 11-10 中缺失的值?参见下面的等式:

{d}_{\left(D,F\right)\to A}=\min \left({d}_{DA},{d}_{FA}\right)=\min \left(3.61,3.20\right)=3.20

注意,基于前面的计算,我们用 d A 和 FA 之间的最小距离替换{D,F}和 A 之间的距离中的缺失值。同样,我们会用其他缺失值进行估算。我们继续这样进行,直到剩下图 11-11 。

A463052_1_En_11_Fig11_HTML.png

图 11-11

The final matrix

产生的集群现在可以表示为图 11-12 。

A463052_1_En_11_Fig12_HTML.jpg

图 11-12

Representing the cluster

分层聚类的主要缺点

层次聚类的主要缺点之一是需要执行大量的计算。

比方说,如果数据集中有 100 个点,那么第一步是确定最接近点 1 的点,以此类推,进行 99 次计算。第二步,我们需要比较第二个点与其余 98 个点的距离。当有 n 个数据点时,总共需要进行 99 × 100 / 2 或 n×(n–1)/2 次计算,以便识别所有数据点组合中距离最小的数据点组合。

随着数据点的数量从 100 增加到 1,000,000,整个计算变得极其复杂。因此,层次聚类仅适用于小型数据集。

K 均值聚类的行业用例

我们已经使用 tot.withinss / totss 指标的肘形曲线计算了 k 的最佳值。让我们用一个类似的计算来建立一个模型。

假设我们正在使用逻辑回归拟合一个模型来预测交易是否是欺诈性的。假设我们将一起处理所有的数据点,这将转化为一个聚类练习,其中 k = 1 在整个数据集上。假设它有 90%的准确率。现在,让我们使用 k = 2 来拟合相同的逻辑回归,其中我们对每个聚类都有不同的模型。我们将测量在测试数据集上使用两个模型的准确性。

我们通过增加 k 值——也就是说,通过增加聚类的数量——来不断重复这个练习。最佳 k 是指我们有 k 个不同的模型,每个聚类一个,并且在测试数据集上达到最高精度的模型。类似地,我们将使用聚类来理解数据集中出现的各种片段。

摘要

在本章中,您学习了以下内容:

  • K-means 聚类有助于对彼此更相似的数据点进行分组,并以彼此更不相似的方式形成组。
  • 聚类可以成为细分、运筹学和数学建模的重要输入。
  • 层次聚类采用与 k-means 聚类相反的方法来形成聚类。
  • 当数据点的数量很大时,生成分层聚类的计算量更大。

十二、主成分分析

当数据点数量与变量数量的比率较高时,回归通常效果最佳。然而,在一些场景中,例如临床试验,数据点的数量是有限的(考虑到从许多个体中收集样本的难度),并且收集的信息量是高的(想想实验室基于收集的少量血液样本给我们提供了多少信息)。

在这些情况下,数据点与变量的比率较低,由于以下原因,人们在使用传统技术时面临困难:

  • 大多数变量很有可能是相互关联的。
  • 运行回归所需的时间可能非常长,因为需要预测的权重数量很大。

在这种情况下,像主成分分析(PCA)这样的技术可以派上用场。PCA 是一种无监督的学习技术,有助于将多个变量分组为更少的变量,而不会丢失原始变量集的太多信息。

在这一章中,我们将看看 PCA 是如何工作的,并了解执行 PCA 的好处。我们也将用 Python 和 r 实现它。

主成分分析的直觉

PCA 是一种通过使用比原始数据集更少的特征或变量来重构原始数据集的方法。要了解其工作原理,请考虑以下示例:

| Var 部门 | Var 1 | Var 2 | | :-- | :-- | :-- | | Zero | one | Ten | | Zero |   2 |   20 | | Zero | three | Thirty | | Zero | four |   40 | | Zero | five |   50 | | one | six |   60 | | one | seven | Seventy | | one | eight |   80 | | one | nine |   90 | | one | Ten | One hundred |

我们将假设 Var 1 和 Var 2 都是用于预测因变量(Dep Var)的自变量。我们可以看到 Var 2 与 Var 1 高度相关,其中 Var 2 = (10) × Var 1。

图 12-1 显示了它们之间的关系。

A463052_1_En_12_Fig1_HTML.jpg

图 12-1

Plotting the relation

在图中,我们可以清楚地看到变量之间有很强的联系。这意味着独立变量的数量可以减少。

该等式可以表示如下:

Var2 = 10 × Var1

换句话说,不是使用两个不同的独立变量,我们可以只使用一个变量 Var1,它会解决这个问题。

此外,如果我们能够通过稍微不同的角度(或者,我们旋转数据集)来观察这两个变量,如图 12-2 中的箭头所示,我们会看到水平方向上有很多变化,而垂直方向上变化很小。

A463052_1_En_12_Fig2_HTML.jpg

图 12-2

Viewpoint/angle from which data points should be looked at

让我们把数据集变得复杂一点。考虑 v1 和 v2 之间的关系如图 12-3 所示的情况。

A463052_1_En_12_Fig3_HTML.jpg

图 12-3

Plotting two variables

同样,这两个变量彼此高度相关,尽管不像前一种情况那样完美相关。

在这种情况下,第一个主成分是解释数据集中最大方差的线/变量,并且是多个独立变量的线性组合。类似地,第二主成分是与第一主成分完全不相关(相关性接近于 0)的线,它解释了数据集中的其余方差,同时也是多个独立变量的线性组合。

通常,第二主成分是垂直于第一主成分的线(因为下一个最高变化发生在垂直于主成分线的方向上)。

一般来说,数据集的第 n 个主成分垂直于同一数据集的第(n–1)个主成分。

PCA 的工作细节

为了理解 PCA 是如何工作的,让我们看另一个例子(在 github 中以“PCA_2vars.xlsx”的形式提供),其中 x1 和 x2 是两个彼此高度相关的独立变量:

A463052_1_En_12_Figa_HTML.png

假设主成分是变量的线性组合,我们将表示如下:

PC 1 = w×x1+w×2

类似地,第二主分量垂直于原始直线,如下所示:

PC 2 =–w2x 1+w1x 2

权重 w 1 和 w 2 被随机初始化,并且应该被进一步迭代以获得最优的权重。

让我们在求解 w 1 和 w 2 时,重新审视一下我们的目标和约束条件:

  • 目标:最大化 PC1 方差。
  • 约束:主成分的总方差应该等于原始数据集中的总方差(因为数据点没有改变,只是我们观察数据点的角度改变了)。

让我们初始化之前创建的数据集中的主要组件:

A463052_1_En_12_Figb_HTML.jpg

PC1 和 PC2 的公式如下所示:

A463052_1_En_12_Figc_HTML.jpg

既然我们已经初始化了主成分变量,我们将引入目标和约束:

A463052_1_En_12_Figd_HTML.jpg

注意,PC 方差= PC1 方差+ PC2 方差。

原始方差= x1 方差+ x2 方差

我们计算原始方差和 PC 方差之间的差异,因为我们的约束是在主成分变换的数据集中保持与原始数据集相同的方差。以下是他们的公式:

A463052_1_En_12_Fige_HTML.jpg

一旦数据集被初始化,我们将继续识别满足我们的目标和约束的 w 1 和 w 2 的最佳值。

让我们看看如何通过 Excel 的规划求解加载项实现这一点:

A463052_1_En_12_Figf_HTML.jpg

请注意,我们之前指定的目标和标准已经达到:

  • PC1 方差最大化。
  • 原始数据集方差和主成分数据集方差之间几乎没有任何差异。(我们只允许小于 0.01 的小差异,以便 Excel 能够解决它,因为可能存在一些舍入误差。)

请注意,PC1 和 PC2 现在高度不相关,PC1 解释了所有变量中最高的方差。此外,x2 在确定 PC1 时比 x1 具有更高的权重(从导出的权重值可以明显看出)。

实际上,一旦得到主成分,它就以相应的平均值为中心,也就是说,主成分列中的每个值都要减去原始主成分列的平均值:

A463052_1_En_12_Figg_HTML.jpg

用于推导上述数据集的公式如下所示:

A463052_1_En_12_Figh_HTML.jpg

PCA 中的缩放数据

PCA 中的主要预处理步骤之一是缩放变量。考虑以下场景:我们对两个变量执行 PCA。一个变量的取值范围为 0-100,另一个变量的取值范围为 0-1。

假设使用 PCA,我们试图捕获数据集中尽可能多的变化,第一个主成分将给予与低方差变量相比具有最大方差的变量(在我们的情况下,Var1)非常高的权重。

因此,当我们计算出主成分的 w 1 和 w 2 时,我们将最终得到接近 0 的 w 1 和接近 1 的 w 2 (其中 w 2 是 PC1 中对应于较高范围变量的权重)。为了避免这种情况,建议调整每个变量,使它们具有相似的范围,这样方差就可以比较。

将主成分分析扩展到多个变量

到目前为止,我们已经看到建立一个 PCA,其中有两个独立变量。在这一节中,我们将考虑如何在有两个以上独立变量的情况下手工构建 PCA。

考虑以下数据集(在 github 中以“PCA_3vars.xlsx”的形式提供):

A463052_1_En_12_Figi_HTML.png

与两变量 PCA 不同,在二维以上的 PCA 中,我们将以稍微不同的方式初始化权重。权重随机初始化,但采用矩阵形式,如下所示:

A463052_1_En_12_Figj_HTML.png

从这个矩阵我们可以认为 PC1 = 0.49 × x1 + 0.89 × x2 + 0.92 × x3。PC2 和 PC3 的计算方法类似。如果有四个独立变量,我们就会有一个 4 × 4 权重矩阵。

让我们看看我们可能有的目标和约束:

A463052_1_En_12_Figk_HTML.jpg

  • 目标:最大化 PC1 方差。
  • 约束:总体 PC 方差应等于总体原始数据集方差。PC1 方差应大于 PC2 方差,PC1 方差应大于 PC3 方差,PC2 方差应大于 PC3 方差。

求解上述问题将得到满足我们标准的最佳重量组合。请注意,Excel 的输出可能与您在 Python 或 R 中看到的输出略有不同,但与 Excel 的输出相比,Python 或 R 的输出可能具有更高的 PC1 方差,这是由于求解中使用的基础算法。还要注意,尽管理想情况下我们希望原始方差和 PC 方差之间的差值为 0,但出于使用 Excel 规划求解执行优化的实际原因,我们允许差值最大为 3。

类似于具有两个独立变量的 PCA 场景,在处理 PCA 之前缩放输入是一个好主意。此外,请注意,PC1 解释了求解权重后的最大变化,因此 PC2 和 PC3 可以被消除,因为它们对原始数据集变化的解释非常少。

Choosing the Number of Principal Components to Consider

选择主成分的个数没有一个统一的方法。在实践中,一个经验法则是选择最少数量的主成分,这些主成分累计解释数据集中 80%的总方差。

在 R 中实现 PCA

PCA 可以使用内置函数prcomp在 R 中实现。看看下面的实现(在 github 中以“PCA R.R”的形式提供):

t=read.csv('D:/Pro ML book/PCA/pca_3vars.csv')
pca=prcomp(t)
pca

pca的输出如下:

A463052_1_En_12_Figm_HTML.jpg

这里的标准偏差值与 PC 变量的标准偏差值相同。旋转值与我们之前初始化的权重值相同。

使用str(pca)可以获得更详细的输出版本,其输出如下所示:

A463052_1_En_12_Fign_HTML.jpg

由此,我们注意到,除了 PC 变量的标准偏差和权重矩阵之外,pca还提供了转换后的数据集。

我们可以通过指定pca$x来访问转换后的数据集。

在 Python 中实现 PCA

使用scikit learn库在 Python 中实现 PCA,如下所示(在 github 中作为“PCA.ipynb”提供):

# import packages and dataset
import pandas as pd
import numpy as np
from sklearn.decomposition import PCA
data=pd.read_csv('F:/course/pca/pca.csv')
from sklearn.decomposition import PCA
pca = PCA(n_components=2)
pca.fit(data)

我们看到,我们拟合了与自变量数量一样多的成分,并在数据上拟合了 PCA。

数据拟合后,将原始数据转换为转换后的数据,如下所示:

A463052_1_En_12_Figo_HTML.jpg

x_pca = pca.transform(data)

pca.components_

components_与主成分关联的权重相同。x_pca是变换后的数据集。

A463052_1_En_12_Figp_HTML.jpg

print(pca.explained_variance_ratio_)

explained_variance_ratio_提供由每个主成分解释的差异量。这非常类似于 R 中的标准差输出,其中 R 给出了每个主成分的标准差。Python 的scikit learn中的 PCA 对其进行了轻微的转换,并给出了每个变量所解释的原始方差的方差。

将 PCA 应用于 MNIST

MNIST 是一个手写数字识别任务。展开一个 28 × 28 的图像,其中每个像素值用一列表示。基于此,我们可以预测输出是否是 0 到 9 之间的一个数字。

假设总共有 784 列,直观上我们应该观察到以下情况之一:

  • 方差为零的列
  • 差异很小的列
  • 方差大的列

在某种程度上,PCA 帮助我们尽可能地消除低方差和无方差的列,同时仍然用有限数量的列实现相当高的精度。

让我们通过下面的例子来看看如何在不损失太多差异的情况下减少列数。github 中的 r”)。

# Load dataset
t=read.csv("D:/Pro ML book/PCA/train.csv")
# Keep the independent variables only, as PCA is on indep. vars
t$Label = NULL
# scale dataset by 255, as it is the mximum possible value in pixels
t=t/255
# Apply PCA
pca=prcomp(t)
str(pca)
# Check the variance explained
cumsum((pca$sdev)²)/sum(pca$sdev²)

上述代码的输出如下:

A463052_1_En_12_Figq_HTML.jpg

由此我们可以看出,前 43 个主成分解释了原始数据集中约 80%的总方差。我们可以在前 43 个主成分上运行模型,而不是在所有 784 列上运行模型,而不会损失太多信息,因此不会损失太多准确性。

摘要

  • PCA 是一种减少数据集中独立变量数量的方法,尤其适用于数据点与独立变量的比率较低的情况。
  • 在应用主成分分析之前,先对独立变量进行缩放是一个好主意。
  • PCA 变换变量的线性组合,使得结果变量表示变量组合内的最大方差。

十三、推荐系统

我们到处都能看到建议。推荐系统旨在

  • 最小化用户搜索产品的努力
  • 提醒用户他们之前关闭的会话
  • 帮助用户发现更多产品

例如,以下是推荐系统的常见实例:

  • 电子商务网站中的推荐窗口小部件
  • 发送到电子邮件地址的推荐项目
  • 社交网站中朋友/联系人的推荐

想象一个场景,电子商务客户没有得到产品推荐。客户将无法执行以下操作:

  • 识别与他们正在查看的产品相似的产品
  • 了解产品价格是否合理
  • 寻找配件或补充产品

这就是为什么推荐系统通常会大幅提升销售额。

在本章中,您将学习以下内容:

  • 要预测用户对某个商品的评价(或用户购买该商品的可能性),请使用
    • 协同过滤
    • 矩阵分解
  • 欧几里德和余弦相似性度量
  • 如何在 Excel、Python 和 R 中实现推荐算法

推荐系统几乎就像一个朋友。它推断你的喜好,并为你提供个性化的选择。构建推荐系统有多种方式,但目标是将用户与一组其他用户关联起来,将一个项目与一组其他项目关联起来,或者两者结合起来。

鉴于推荐是将一个用户/项目与另一个用户/项目相关联,它转化为 k 个最近邻居的问题:识别非常相似的少数几个,然后基于大多数最近邻居表现出的偏好进行预测。

了解 k 近邻

最近邻是离所考虑的实体最近的实体(在数据集的情况下是数据点)。如果两个实体之间的距离很小,则它们是接近的。

考虑具有以下属性的三个用户:

| 用户 | 重量 | | :-- | :-- | | A | Sixty | | B | Sixty-two | | C | Ninety |

我们可以直观地得出结论,与 c 相比,用户 A 和 B 在权重方面更加相似。

让我们再添加一个用户属性—年龄:

| 用户 | 重量 | 年龄 | | :-- | :-- | :-- | | A | Sixty | Thirty | | B | Sixty-two | Thirty-five | | C | Ninety | Thirty |

用户 A 和 B 之间的“距离”可以测量为:

\sqrt{\left({\left(62-60\right)}²+{\left(35-30\right)}²\right)}

这种用户间距离的计算方式类似于两点间距离的计算方式。

然而,在使用多个变量计算距离时,您需要稍微小心一点。以下示例可以突出距离计算的缺陷:

| 汽车模型 | 可达到的最高速度 | 档位数量 | | :-- | :-- | :-- | | A | One hundred | four | | B | One hundred and ten | five | | C | One hundred | five |

在上表中,如果我们使用传统的“距离”指标来衡量汽车之间的相似性,我们可能会得出结论,A 型和 C 型最相似(即使它们的档位数量不同)。然而,凭直觉我们知道,B 和 C 比 A 和 C 更相似,因为它们有相同数量的齿轮,它们的最大可达速度也相似。

这种差异突出了变量规模的问题,其中一个变量与另一个变量相比具有非常高的幅度。为了解决这个问题,我们通常会在进一步计算距离之前对变量进行归一化处理。规范化变量是一个将所有变量统一起来的过程。

标准化变量有多种方法:

  • 将每个变量除以该变量的最大值(取–1 和 1 之间的所有值)
  • 找出变量的每个数据点的 Z 值。z 得分是(数据点的值-变量的平均值)/(变量的标准偏差)。
  • 将每个变量除以该变量的(最大–最小)值(称为最小最大缩放)。

诸如此类的步骤有助于规范化变量,从而防止缩放带来的问题。

一旦获得一个数据点到其他数据点的距离——在推荐系统的情况下,也就是说,一旦识别出与给定项目最近的项目——如果系统得知用户在历史上喜欢大多数最近的邻近项目,那么它将向用户推荐这些项目。

k-nearest neighbors 中的 k 代表在对用户是否喜欢最近邻居进行多数投票时要考虑的最近邻居的数量。例如,如果用户喜欢一个项目的 10 (k)个最近邻居中的 9 个,我们将向用户推荐该项目。类似地,如果用户只喜欢 10 个最近邻中的 1 个,我们不会向用户推荐这个项目(因为喜欢的项目是少数)。

基于邻居的分析考虑了多个用户可以合作帮助预测用户是否喜欢某样东西的方式。

有了这个背景,我们将继续前进,看看推荐系统算法的演变。

基于用户的协同过滤的工作细节

基于用户当然是指以用户为基础的东西。协作意味着利用用户之间的某种关系(相似性)。而过滤是指从所有用户中过滤掉一部分用户。

为了了解基于用户的协同过滤(UBCF),考虑下面的例子(在 github 中以“ubcf.xlsx”的形式提供):

| 用户/电影 | 只是我的运气 | 水中的女士 | 飞机上的蛇 | 超人归来 | 夜间听众 | 你我和杜普利 | | :-- | :-- | :-- | :-- | :-- | :-- | :-- | | 克劳迪娅 Puig | three |   | Three point five | four | Four point five | Two point five | | 吉恩·西摩 | One point five | three | Three point five | five | three | Three point five | | 杰克·马修斯 |   | three | four | five | three | Three point five | | 丽莎·罗斯 | three | Two point five | Three point five | Three point five | three | Two point five | | 米克拉斯拉 | Two | three | four | three | three | Two | | 托比 |   |   | Four point five | four |   | one |

假设我们想知道用户 Claudia Puig 对电影《水中女士》的评价。我们将从找出与克劳迪娅最相似的用户开始。用户相似性可以用几种方法计算。以下是计算相似性的两种最常见的方法:

  • 用户之间的欧几里德距离
  • 用户之间的余弦相似度

欧几里德距离

计算 Claudia 与每个其他用户的欧几里德距离的方法如下(可在 github 中“ubcf.xlsx”文件的“欧几里德距离”表中找到):

A463052_1_En_13_Figa_HTML.jpg

由于空间和格式的限制,我们看不到完整的图片,但基本上相同的公式适用于各列。

对于每部电影,每个其他用户到 Claudia 的距离如下:

A463052_1_En_13_Figb_HTML.jpg

注意,总距离值是两个用户评价给定电影的所有距离的平均值。鉴于 Lisa Rose 是与 Claudia 整体距离最小的用户,我们将考虑 Lisa 提供的评分作为 Claudia 很可能给电影《水中女士》的评分。

在这种计算中要考虑的一个主要问题是,一些用户可能是温和的批评者,而一些用户可能是严厉的批评者。用户 A 和 B 可能隐含地具有观看给定电影的相似经历,但是显而易见地,他们的评级可能不同。

对用户进行规范化

考虑到用户的批评程度不同,我们需要确保解决这个问题。标准化在这里会有所帮助。

我们可以为用户规范化如下:

  1. 取给定用户的所有电影的平均评级。
  2. 以每部电影与用户平均评分之间的差异为例。

通过获取单个电影的评级和用户的平均评级之间的差异,我们将知道他们是喜欢一部电影多于他们观看的平均电影、少于他们观看的平均电影还是等于他们观看的平均电影。

让我们看看这是如何做到的:

A463052_1_En_13_Figc_HTML.jpg

上述公式如下(可在 github 中“ubcf.xlsx”文件的“规范化用户”表中找到):

A463052_1_En_13_Figd_HTML.jpg

现在,我们已经对给定用户进行了规范化,我们计算哪个用户与 Claudia 最相似,方法与前面计算用户相似度的方法相同。唯一的区别是,现在我们将基于标准化评级(而不是原始评级)计算距离:

A463052_1_En_13_Fige_HTML.jpg

我们可以看到,Lisa Rose 仍然是与 Claudia Puig 距离最近(或最接近,或最相似)的用户。丽莎对《水中女士》的评分比她电影的平均评分 3.00 低 0.50 个单位,这比她的平均评分低了约 8%。鉴于 Lisa 是与 Claudia 最相似的用户,我们预计 Claudia 的评分同样会比她的平均评分低 8%,计算结果如下:

3.5 × (1 – 0.5 / 3) = 2.91

考虑单一用户的问题

到目前为止,我们已经考虑了与 Claudia 最相似的单个用户。在实践中,越多越好——也就是说,确定 k 个与给定用户最相似的用户给出的加权平均评级比确定最相似用户的评级更好。

但是,我们需要注意的是,并不是所有的 k 用户都是同样相似的。有些比较像,有些不太像。换句话说,一些用户的评级应该被赋予更大的权重,而其他用户的评级应该被赋予更小的权重。但是使用基于距离的度量,没有简单的方法来得出相似性度量。

余弦相似性作为一种度量标准,在解决这个问题上派上了用场。

余弦相似性

我们可以通过一个例子来看余弦相似性。考虑以下矩阵:

|   | 电影 | 电影 2 | 电影 3 | | :-- | :-- | :-- | :-- | | 用户 1 | one | Two | Two | | 用户 2 | Two | four | four |

在上表中,我们看到两个用户的评分高度相关。但是,评级的幅度是有区别的。

如果我们要计算两个用户之间的欧几里德距离,我们会注意到这两个用户彼此非常不同。但是我们可以看到,这两个用户在评分的方向(趋势)上是相似的,虽然在评分的幅度上并不相似。使用用户之间的余弦相似性可以解决用户趋势相似但幅度不相似的问题。

两个用户之间的余弦相似度定义如下:

similarity= \cos \left(\theta \right)=\frac{A\cdot B}{{\left\Vert A\right\Vert}_2\;{\left\Vert B\right\Vert}_2}=\frac{{\displaystyle \sum_{i=1}^n{A}_i{B}_i}}{\sqrt{{\displaystyle \sum_{i=1}^n{A_i}²}}\;\sqrt{{\displaystyle \sum_{i=1}^n{B_i}²}}}

a 和 B 分别是对应于用户 1 和用户 2 的向量。

让我们看看如何计算前面矩阵的相似性:

  • 给定公式的分子= (1 × 2 + 2 × 4 + 2 × 4) = 18

  • Denominator of the given formula =

    \sqrt{\left({1}²+{2}²+{2}²\right)}

    ×

    \sqrt{\left({2}²+{4}²+{4}²\right)}=\sqrt{(9)}\times \sqrt{(36)}=3\times 6=18

  • 相似度= 18 / 18 = 1。

基于给定的公式,我们可以看到,基于余弦相似性,我们能够将高相似性分配给方向相关但不一定在幅度上相关的用户。

我们之前最初计算(在欧几里德距离计算中)的评级矩阵上的余弦相似性将以与我们计算前述公式类似的方式进行计算。余弦相似度计算的步骤保持不变:

  1. 正常化用户。
  2. 计算给定用户的其余用户的余弦相似度。

为了说明我们如何计算余弦相似度,让我们计算 Claudia 与其他每个用户的相似度(可在 github 中“ubcf.xlsx”文件的“余弦相似度”表中找到):

  1. Normalize user ratings:

    A463052_1_En_13_Figf_HTML.jpg

  2. Calculate the numerator part of the cosine similarity calculation:

    A463052_1_En_13_Figg_HTML.jpg

    The numerator would be as follows:

    A463052_1_En_13_Figh_HTML.jpg

  3. Prepare the denominator calculator of cosine similarity:

    A463052_1_En_13_Figi_HTML.jpg

  4. Calculate the final cosine similarity, as follows:

    A463052_1_En_13_Figj_HTML.jpg

我们现在有了一个介于–1 和+1 之间的相似性值,它给出了给定用户的相似性得分。

我们现在已经克服了在预测给定用户可能给电影的评级时必须考虑多个用户给出的评级所面临的问题。现在可以计算与给定用户更相似的用户。

现在,预测克劳迪娅可能给水电影中的电影女士的评级的问题可以通过以下步骤来解决:

  1. 正常化用户。
  2. 计算余弦相似度。
  3. 计算加权平均标准化评级。

假设我们试图通过使用两个最相似的用户而不是一个来预测评级。我们将遵循以下步骤:

  1. 找出两个最相似的用户,他们也对电影《水中女士》进行了评级。
  2. 计算他们给电影的加权平均标准化评分。

在这种情况下,Lisa 和 Mick 是与 Claudia 最相似的两个用户,他们在水中评价 Lady。(注意,即使 Toby 是最相似的用户,他也没有对水中的女士进行评级,因此我们不能考虑将他用于评级预测。)

加权平均评级计算

让我们来看看给定的标准化评分和两个最相似用户的相似性:

|   | 类似 | 标准化评级 | | :-- | :-- | :-- | | 丽莎·罗斯 | Zero point four seven | –0.5 | | 米克拉斯拉 | Zero point five six | Zero point one seven |

加权平均评级现在如下:

(0.47 × –0.5 + 0.56 × 0.17) / (0.47 + 0.56) = –0.14

潜在地,克劳迪娅的平均评分现在将减少 0.14,以得出电影《水中女士》中克劳迪娅的预测评分。

得出加权平均评分的另一种方法是基于平均评分的百分比,如下所示:

|   | 类似 | 标准化评级 | 平均分 | 平均评级百分比 | | :-- | :-- | :-- | :-- | :-- | | 丽莎·罗斯 | Zero point four seven | –0.5 | three | –0.5 / 3 = –0.16 | | 米克拉斯拉 | Zero point five six | Zero point one seven | Two point eight three | 0.17 / 2.83 = 0.06 |

加权平均标准化评级百分比现在如下:

(0.47 × –0.16 + 0.56 × 0.06) / (0.47 + 0.56) = –0.04

因此,克劳迪娅的平均评级可能会降低 4%,以得出电影《水中女士》的预测评级。

选择正确的方法

在推荐系统中,没有固定的技术被证明总是有效的。这需要一个典型的训练、验证和测试场景来得出最佳的参数组合。

可以测试的参数组合如下:

  • 要考虑的相似用户的最佳数量
  • 在用户有资格被考虑用于类似的用户计算之前,由用户一起评级的共同电影的最佳数量
  • 加权平均评级计算方法(基于百分比或绝对值)

我们可以遍历参数的各种组合的多个场景,计算测试数据集的准确性,并决定给出最小错误率的组合是给定数据集的最佳组合。

计算误差

有多种计算方法,首选方法因业务应用而异。让我们看两个案例:

  • 对测试数据集进行的所有预测的均方误差(MSE)
  • 用户在下次购买时购买的推荐商品数量

请注意,虽然 MSE 有助于构建算法,但在实践中,我们可能会将模型的性能作为与业务相关的结果来衡量,如第二种情况。

与 UBCF 的问题

基于用户的协同过滤的一个问题是,每个用户必须与每个其他用户进行比较,以识别最相似的用户。假设有 100 个客户,这意味着第一个用户与 99 个用户进行比较,第二个用户与 98 个用户进行比较,第三个用户与 97 个用户进行比较,依此类推。这里的总比较如下:

99 + 98 + 97 + … + 1 + 0 = 99 × (99 + 1) / 2 = 4950

对于一百万个客户,比较的总数如下所示:

999,999 × 1,000,000 / 2 = ~500,000,000,000

大约有 5000 亿次比较。计算表明,随着客户数量的增加,识别最相似客户的比较次数呈指数增加。在生产中,这成为一个问题,因为如果每个用户与每个其他用户的相似性需要每天计算(因为用户偏好和评级每天都根据最新的用户数据更新),那么每天需要执行大约 5000 亿次比较。

为了解决这个问题,我们可以考虑基于项目的协同过滤,而不是基于用户的协同过滤。

基于项目的协同过滤

考虑到计算的数量在 UBCF 是一个问题,我们将修改这个问题,以便我们观察项目之间的相似性,而不是用户。基于项目的协同过滤(IBCF)背后的思想是,如果两个项目从相同的用户那里得到的评级是相似的,则这两个项目是相似的。鉴于 IBCF 是基于项目而不是用户相似性,它不存在执行数十亿次计算的问题。

让我们假设一个数据库中总共有 10,000 部电影,该网站吸引了 100 万客户。在这种情况下,如果我们执行 UBCF,我们将执行大约 5000 亿次相似性计算。但是使用 IBCF,我们将执行 9999×5000 = ~ 5000 万次相似性计算。

我们可以看到,随着客户数量的增长,相似性计算的数量呈指数增长。然而,考虑到项目数量(在我们的例子中是电影名称)预计不会经历与客户数量相同的增长率,一般来说,IBCF 的计算敏感度低于 UBCF。

IBCF 的计算方式和涉及的技术与 UBCF 非常相似。唯一的区别是,我们将处理前一节中看到的原始电影矩阵的转置形式。这样,这些行不是用户的,而是电影的。

注意,虽然 IBCF 在计算方面比 UBCF 好,但计算量仍然很高。

在 R 中实现协同过滤

在这一节中,我们将查看用于在 r 中实现 UBCF 的函数。我在下面的代码中实现了在recommenderlab包中可用的函数,但在实践中,建议您从头构建一个推荐函数,以便为手头的问题进行定制(代码可作为“UBCF”获得。github 中的 r”)。

# Import data and required packages
t=read.csv("D:/book/Recommender systems/movie_rating.csv")
library(reshape2)
library(recommenderlab)
# Reshape data into a pivot format
t2=acast(t,critic~title)
t2
# Convert it to a matrix
R<-as.matrix(t2)

# Convert R into realRatingMatrix structure
# realRatingMatrix is a recommenderlab sparse-matrix like data structure

r<-as(R,"realRatingMatrix")

# Implement the UBCF method
rec=Recommender(r[1:nrow(r)],method="UBCF")

# Predict the missing rating
recom<-predict(rec,r[1:nrow(r)],type="ratings")
str(recom)

在这段代码中,我们对数据进行了整形,以便将其转换成一个由Recommender函数使用的realRatingMatrix类,从而提供缺失值预测。

用 Python 实现协同过滤

我们在 R 中使用了一个预测包,但对于 Python,我们将手工构建一种方法来预测用户可能给出的评级。在下面的代码中,我们将通过仅考虑与 Claudia 最相似的用户(代码在 github 中为“UBCF.ipynb ”)来创建一种方法,以预测 Claudia 可能对水电影中的女士给出的评级。

  1. 导入数据集:

    import pandas as pd
    import numpy as np
    t=pd.read_csv("D:/book/Recommender systems/movie_rating.csv")
    
    
  2. 将数据集转换成数据透视表:

    t2 = pd.pivot_table(t,values='rating',index='critic',columns='title')
    
    
  3. 重置索引:

    t3 = t2.reset_index()
    t3=t3.drop(['critic'],axis=1)
    
    
  4. 归一化数据集:

    t4=t3.subtract(np.mean(t3,axis=1),axis=0)
    
    
  5. 删除缺少水中女士值的行:

    t5=t4.loc[t4['Lady in the Water'].dropna(axis=0).index]
    t6=t5.reset_index()
    t7=t6.drop(['index'],axis=1)
    
    
  6. 计算每个其他用户到克劳迪娅的距离:

    x=[]
    for i in range(t7.shape[0]):
        x.append(np.mean(np.square(t4.loc[0]-t7.loc[i])))
    t6.loc[np.argmin(x)]['Lady in the Water']
    
    
  7. 计算克劳迪娅的预测评分:

    np.mean(t3.loc[0]) * (1+(t6.loc[np.argmin(x)]['Lady in the Water']/np.mean(t3.loc[3])))
    
    

矩阵分解的工作细节

尽管基于用户或基于项目的协同过滤方法简单而直观,但矩阵分解技术通常更有效,因为它们允许我们发现用户和项目之间交互的潜在特征。

在矩阵分解中,如果有 U 个用户,每个用户被表示在 K 列中,因此我们有一个 U × K 用户矩阵。同样,如果有 D 项,每一项也用 K 列表示,给我们一个 D × K 的矩阵。

用户矩阵的矩阵乘法和项目矩阵的转置将产生 U × D 矩阵,其中 U 个用户可能已经对 D 个项目中的一些进行了评级。

这 K 列基本上可以转化为 K 个特征,其中一个或另一个特征中较高或较低的幅度可以给我们一个项目类型或流派的指示。这使我们能够知道用户会给哪些功能更高的权重,或者用户可能不喜欢哪些功能。本质上,矩阵分解是一种以这样的方式来表示用户和项目的方式,即如果对应于项目的特征是用户给予较高权重的特征,则用户喜欢或购买项目的概率很高。

我们将通过一个例子来看看矩阵分解是如何工作的。让我们假设我们有一个用户(U)和电影(D)的矩阵,如下(数据集在 github 中以“matrix factorization example . xlsx”的形式提供):

| 用户 | 电影 | 实际的 | | :-- | :-- | :-- | | one | one | five | | one | Two | three | | one | three |   | | one | four | one | | Two | one | four | | Two | Two |   | | Two | three |   | | Two | four | one | | three | one | one | | three | Two | one | | three | three |   | | three | four | five | | four | one | one | | four | Two |   | | four | three |   | | four | four | four | | five | one |   | | five | Two | one | | five | three | five | | five | four | four |

我们的任务是预测实际列中缺少的值,这些值表明用户还没有对电影进行评级。

在这种情况下,矩阵分解的数学计算如下:

  • 目的:改变 P 和 Q 矩阵的随机初始值,以最小化总误差。
  • 约束:任何预测都不能大于 5 或小于 1。
  1. Initialize the values of P matrix randomly, where P is a U × K matrix. We’ll assume a value of k = 2 for this example. A better way of randomly initializing the values is by limiting the values to be between 0 and 1. In this scenario, the matrix of P will be a 5 × 2 matrix, because k = 2 and there are 5 users:

    A463052_1_En_13_Figk_HTML.jpg

  2. Initialize the values of Q matrix randomly, again where Q is a K × D matrix—that is, a 2 × 4 matrix, because there are four movies, as shown in the first table. The Q matrix would be as follows:

    A463052_1_En_13_Figl_HTML.jpg

  3. Calculate the value of the matrix multiplication of P × Q matrix. Note that the Prediction column in the following is calculated by the matrix multiplication of P matrix and Q matrix (I will discuss the Constraint column in the next step):

    A463052_1_En_13_Figm_HTML.jpg

  4. Specify the optimization constraints. The predicted value (the multiplication of each element of the two matrices) should ideally be equal to the ratings of the big matrix. The error calculation is based on the typical squared error calculation and is done as follows (note that the weight values in P and matrices have varied because they are random numbers and are initialized using the randbetween function, which changes values every time Enter is pressed in Excel):

    A463052_1_En_13_Fign_HTML.jpg

上述目标和约束可以在规划求解中指定为优化方案,如下所示:

A463052_1_En_13_Figo_HTML.jpg

注意,一旦我们针对给定的目标和约束进行了优化,P 和 Q 矩阵中的权重的最优值就被得出,并且如下:

A463052_1_En_13_Figp_HTML.jpg

Insights on P AND Q MATRICES

在 P 矩阵中,用户 1 和用户 2 对于因子 1 和 2 具有相似的权重,因此他们可能被认为是相似的用户。

此外,用户 1 和用户 2 对电影进行评级的方式非常相似——用户 1 评级高的电影也具有来自用户 2 的高评级。类似地,用户 1 评价差的电影也具有来自用户 2 的低评价。

这同样适用于对 Q 矩阵(电影矩阵)的解释。电影 1 和电影 4 之间有相当大的距离。我们还可以看到,对于大多数用户来说,如果电影 1 的评分很高,那么电影 4 的评分就很低,反之亦然。

用 Python 实现矩阵分解

请注意,P 矩阵和 Q 矩阵是通过 Excel 的求解器获得的,它实际上是在后端运行梯度下降。换句话说,我们正在以一种类似于基于神经网络的方法来推导权重,在这种方法中,我们试图最小化总体平方误差。

让我们看看在 keras 中为以下数据集实现矩阵分解(代码在 github 中以“matrix factorization.ipynb”的形式提供):

| 用户 | 电影 | 实际的 | | :-- | :-- | :-- | | one | four | one | | Two | four | one | | three | one | one | | three | Two | one | | four | one | one | | five | Two | one | | one | Two | three | | Two | one | four | | four | four | four | | five | four | four | | one | one | five | | three | four | five | | five | three | five |
# Import the required packages and dataset
import pandas as pd
ratings= pd.read_csv('/content/datalab/matrix_factorization_keras.csv')
# Extract the unique users
users = ratings.User.unique()
# Extract the unique movies
articles = ratings.Movies.unique()
# Index each user and article
userid2idx = {o:i for i,o in enumerate(users)}
articlesid2idx = {o:i for i,o in enumerate(articles)}
# Apply the index created to the original dataset
ratings.Movies = ratings.Movies.apply(lambda x: articlesid2idx[x])
ratings.User = ratings.User.apply(lambda x: userid2idx[x])
# Extract the number of unique users and articles
n_users = ratings.User.nunique()
n_articles = ratings.Movies.nunique()
# Define the error metric
import keras.backend as K
def rmse(y_true,y_pred):
    score = K.sqrt(K.mean(K.pow(y_true - y_pred, 2)))
    return score
# Import relevant packages
from keras.layers import Input, Embedding, Dense, Dropout, merge, Flatten
from keras.models import Model

函数Embedding有助于创建向量,类似于我们在第八章中把一个单词转换成低维向量的方法。

通过下面的代码,我们将能够创建 P 矩阵和 Q 矩阵的初始化:

def embedding_input(name,n_in,n_out):
    inp = Input(shape=(1,),dtype='int64',name=name)
    return inp, Embedding(n_in,n_out,input_length=1)(inp)
n_factors = 2
user_in, u = embedding_input('user_in', n_users, n_factors)
article_in, a = embedding_input('article_in', n_articles, n_factors)
# Initialize the dot product between user matrix and movie matrix
x = merge.dot([u,a],axes=2)
x=Flatten()(x)
# Initialize the model specification
from keras import optimizers
model = Model([user_in,article_in],x)
sgd = optimizers.SGD(lr=0.01)
model.compile(sgd,loss='mse',metrics=[rmse])
model.summary()
# Fit the model by specifying inputs and output
model.fit([ratings.User,ratings.Movies], ratings.Actual, nb_epoch=1000, batch_size=13)

现在模型已经建立,让我们提取用户和电影矩阵(P 和 Q 矩阵)的权重:

A463052_1_En_13_Figq_HTML.jpg

# User matrix
model.get_weights()[0]

A463052_1_En_13_Figr_HTML.jpg

# Movie matrix
model.get_weights()[1]

在 R 中实现矩阵分解

虽然矩阵分解可以使用kerasR包来实现,但我们将使用recommenderlab包(与我们用于协同过滤的包相同)。

下面的代码实现了 R 中的矩阵分解(可作为“矩阵分解”使用。github 中的 r”)。

  1. 导入相关包和数据集:

    # Matrix factorization
    t=read.csv("D:/book/Recommender systems/movie_rating.csv")
    library(reshape2)
    library(recommenderlab)
    
    
  2. 数据预处理:

    t2=acast(t,critic~title)
    t2
    # Convert it as a matrix
    R<-as.matrix(t2)
    # Convert R into realRatingMatrix data structure
    # RealRatingMatrix is a recommenderlab sparse-matrix like data-structure
    r <- as(R, "realRatingMatrix")
    
    
  3. Use the funkSVD function to build the matrix factors:

    fsvd <- funkSVD(r, k=2,verbose = TRUE)
    
    p <- predict(fsvd, r, verbose = TRUE)
    p
    
    

    Note that the object p constitutes the predicted ratings of all the movies across all the users. The object fsvd constitutes the user and item matrices , and they can be obtained with the following code:

    A463052_1_En_13_Figs_HTML.jpg

    str(fsvd)
    
    

因此,用户矩阵可通过fsvd$U访问,项目矩阵可通过fsvd$V访问。这些参数是我们在第七章中了解到的学习率和纪元参数。

摘要

在本章中,您学习了以下内容:

  • 用于提供推荐的主要技术是协同过滤和矩阵分解。
  • 就大量的计算而言,协同过滤是非常禁止的。
  • 矩阵分解的计算量较小,并且通常提供更好的结果。
  • 在 Excel、Python 和 R 中构建矩阵分解和协作过滤算法的方法