使用 CNN(卷积神经网络)的情感分析/文本分类
文本分类有很多应用。例如,仇恨言论检测、意图分类和组织新闻文章。本文的重点是情感分析,这是一个文本分类问题。我们将 IMDB 意见分为两类,即正面和负面。
我们使用 Python 和 Jupyter Notebook 来开发我们的系统,我们将使用的库包括 Keras、 Gensim、 Numpy、 Pandas 、 Regex (re)和 NLTK 。我们还将使用 Google News Word2Vec 型号。完整的代码和数据可以从这里下载。
数据探索
首先,我们看一下我们的数据。由于数据文件是制表符分隔文件(tsv ),我们将使用 pandas 来读取它,并传递参数来告诉函数分隔符是制表符,并且在我们的数据文件中没有标题。然后,我们设置数据帧的报头。
import pandas as pd
data = pd.read_csv('imdb_labelled.tsv',
header = None,
delimiter='\t')
data.columns = ['Text', 'Label']
df.head()
然后我们检查数据的形状
data.shape
现在我们看到了阶级分布。我们有 386 个正面和 362 个负面的例子。
data.Label.value_counts()
数据清理
数据清理的第一步是删除标点符号。我们只需使用正则表达式就可以了。删除标点符号后,数据将保存在同一数据框中。
import redef remove_punct(text):
text_nopunct = ''
text_nopunct = re.sub('['+string.punctuation+']', '', text)
return text_nopunctdata['Text_Clean'] = data['Text'].apply(lambda x: remove_punct(x))
在下一步中,我们通过使用 NLTK 的 word_tokenize 来标记注释。如果我们传递一个字符串‘Tokenizing is easy’给 word_tokenize。输出是[‘标记化’,‘是’,‘简单’]
from nltk import word_tokenizetokens = [word_tokenize(sen) for sen in data.Text_Clean]
然后我们用小写字母表示数据。
def lower_token(tokens):
return [w.lower() for w in tokens]
lower_tokens = [lower_token(token) for token in tokens]
在对数据进行小写处理后,使用 NLTK 的停止字从数据中删除停止字。
from nltk.corpus import stopwordsstoplist = stopwords.words('english')def removeStopWords(tokens):
return [word for word in tokens if word not in stoplist]filtered_words = [removeStopWords(sen) for sen in lower_tokens]data['Text_Final'] = [' '.join(sen) for sen in filtered_words]
data['tokens'] = filtered_words
因为我们的问题是二元分类。我们需要给我们的模型传递一个二维输出向量。为此,我们在数据框中添加了两个 one hot 编码列。
pos = []
neg = []
for l in data.Label:
if l == 0:
pos.append(0)
neg.append(1)
elif l == 1:
pos.append(1)
neg.append(0)data['Pos']= pos
data['Neg']= neg
data = data[['Text_Final', 'tokens', 'Label', 'Pos', 'Neg']]
data.head()
将数据分为测试和训练
现在,我们将数据集分为训练集和测试集。我们将使用 90 %的数据进行训练,10 %的数据进行测试。我们使用随机状态,所以每次我们都得到相同的训练和测试数据。
data_train, data_test = train_test_split(data,
test_size=0.10,
random_state=42)
然后构建训练词汇,得到最大训练句子长度和总字数的训练数据。
all_training_words = [word for tokens in data_train["tokens"] for word in tokens]
training_sentence_lengths = [len(tokens) for tokens in data_train["tokens"]]
TRAINING_VOCAB = sorted(list(set(all_training_words)))
print("%s words total, with a vocabulary size of %s" % (len(all_training_words), len(TRAINING_VOCAB)))
print("Max sentence length is %s" % max(training_sentence_lengths))
然后建立测试词汇,得到测试数据中最大的测试句子长度和总字数。
all_test_words = [word for tokens in data_test[“tokens”] for word in tokens]
test_sentence_lengths = [len(tokens) for tokens in data_test[“tokens”]]
TEST_VOCAB = sorted(list(set(all_test_words)))
print(“%s words total, with a vocabulary size of %s” % (len(all_test_words), len(TEST_VOCAB)))
print(“Max sentence length is %s” % max(test_sentence_lengths))
正在加载 Google 新闻 Word2Vec 模型
现在我们将加载 Google News Word2Vec 模型。这一步可能需要一些时间。如果您有足够的数据量,您可以使用任何其他预先训练的单词嵌入或训练您自己的单词嵌入。
word2vec_path = 'GoogleNews-vectors-negative300.bin.gz'
word2vec = models.KeyedVectors.load_word2vec_format(word2vec_path, binary=True)
标记化和填充序列
每个单词被赋予一个整数,这个整数被放在一个列表中。因为所有的训练句子必须具有相同的输入形状,所以我们填充句子。
例如,如果我们有一个句子“文本如何排序和填充”。每个单词都有一个编号。我们假设 how = 1,text = 2,to = 3,sequence =4,and = 5,padding = 6,works = 7。调用 texts_to_sequences 后,我们的句子看起来会像[1,2,3,4,5,6,7 ]。现在我们假设我们的最大序列长度= 10。填充后,我们的句子将看起来像[0,0,0,1,2,3,4,5,6,7 ]
我们对测试数据也做同样的事情。欲了解完整代码,请访问。
tokenizer = Tokenizer(num_words=len(TRAINING_VOCAB), lower=True, char_level=False)
tokenizer.fit_on_texts(data_train[“Text_Final”].tolist())
training_sequences = tokenizer.texts_to_sequences(data_train[“Text_Final”].tolist())train_word_index = tokenizer.word_index
print(‘Found %s unique tokens.’ % len(train_word_index))train_cnn_data = pad_sequences(training_sequences,
maxlen=MAX_SEQUENCE_LENGTH)
现在,我们将从 Google News Word2Vec 模型中获取嵌入内容,并根据我们分配给每个单词的序列号保存它们。如果我们不能得到嵌入,我们为这个词保存一个随机向量。
train_embedding_weights = np.zeros((len(train_word_index)+1,
EMBEDDING_DIM))for word,index in train_word_index.items():
train_embedding_weights[index,:] = word2vec[word] if word in word2vec else np.random.rand(EMBEDDING_DIM)print(train_embedding_weights.shape)
定义 CNN
文本作为一个序列被传递给 CNN。嵌入矩阵被传递给嵌入层。五种不同的过滤器大小应用于每个评论,GlobalMaxPooling1D 层应用于每个层。所有的输出然后被连接。然后施加脱落层、致密层、脱落层和最终致密层。
model.summary()将打印所有图层的简要摘要以及输出的形状。
def ConvNet(embeddings, max_sequence_length, num_words, embedding_dim, labels_index):
embedding_layer = Embedding(num_words,
embedding_dim,
weights=[embeddings],
input_length=max_sequence_length,
trainable=False)
sequence_input = Input(shape=(max_sequence_length,), dtype='int32')
embedded_sequences = embedding_layer(sequence_input) convs = []
filter_sizes = [2,3,4,5,6] for filter_size in filter_sizes:
l_conv = Conv1D(filters=200,
kernel_size=filter_size,
activation='relu')(embedded_sequences)
l_pool = GlobalMaxPooling1D()(l_conv)
convs.append(l_pool) l_merge = concatenate(convs, axis=1) x = Dropout(0.1)(l_merge)
x = Dense(128, activation='relu')(x)
x = Dropout(0.2)(x)
preds = Dense(labels_index, activation='sigmoid')(x) model = Model(sequence_input, preds)
model.compile(loss='binary_crossentropy',
optimizer='adam',
metrics=['acc'])
model.summary()
return model
现在我们将执行该函数。
model = ConvNet(train_embedding_weights,
MAX_SEQUENCE_LENGTH,
len(train_word_index)+1,
EMBEDDING_DIM,
len(list(label_names)))
训练 CNN
历元数是您的模型将循环和学习的数量,批量大小是您的模型在单个时间看到的数据量。因为我们只在几个时期内对小数据集进行训练,所以模型会过度拟合。
num_epochs = 3
batch_size = 32
hist = model.fit(x_train,
y_tr,
epochs=num_epochs,
validation_split=0.1,
shuffle=True,
batch_size=batch_size)
测试模型
哇!仅用三次迭代和一个小数据集,我们就能获得 84 %的准确率。
predictions = model.predict(test_cnn_data,
batch_size=1024,
verbose=1)
labels = [1, 0]
prediction_labels=[]
for p in predictions:
prediction_labels.append(labels[np.argmax(p)])sum(data_test.Label==prediction_labels)/len(prediction_labels)
CNN 情感分析
使用卷积神经网络分析 IMDb 数据集中的情感
卷积神经网络(CNN)构成了多个现代计算机视觉系统的主干。图像分类、目标检测、语义分割——所有这些任务都可以由 CNN 成功完成。乍一看,对自然语言处理这样不同的任务使用相同的技术似乎是违反直觉的。这篇文章是我试图用著名的 IMDb 数据集来解释这种方法背后的直觉。
Source: https://www.analyticsvidhya.com/blog/2018/07/hands-on-sentiment-analysis-dataset-python/
读完这篇文章后,你会:
- 了解如何使用 torchtext 预处理文本
- 理解卷积背后的思想
- 了解如何将文本表示为图像
- 在 PyTorch 中构建一个基本的 CNN 情感分析模型
我们开始吧!
数据
用于二元情感分类的 IMDb 数据集包含一组 25,000 条用于训练的高度极性电影评论和 25,000 条用于测试的高度极性电影评论。幸运的是,它是 torchtext 的一部分,所以在 PyTorch 中加载和预处理它很简单:
# Create an instance that turns text into tensors
TEXT = data.Field(tokenize = 'spacy', batch_first = True)
LABEL = data.LabelField(dtype = torch.float)# Load data from torchtext
train_data, test_data = datasets.IMDB.splits(TEXT, LABEL)
train_data, valid_data = train_data.split()# Select only the most important 30000 words
MAX_VOCAB_SIZE = 30_000# Build vocabulary
TEXT.build_vocab(train_data,
max_size = MAX_VOCAB_SIZE,
# Load pretrained embeddings
vectors = "glove.6B.100d",
unk_init = torch.Tensor.normal_)LABEL.build_vocab(train_data)
data.Field
类定义了一个数据类型以及将它转换成张量的指令。在这种情况下,我们使用 SpaCy tokenizer 将文本分割成单独的标记(单词)。之后,我们构建一个词汇表,这样我们就可以在以后将令牌转换成整数。词汇表是用训练数据集中的所有单词构建的。此外,我们加载预训练的手套嵌入,这样我们就不需要从头开始训练我们自己的单词向量。如果你想知道什么是单词嵌入,它们是一种单词表示形式,是人类理解语言和机器理解语言之间的桥梁。要了解更多信息,请阅读本文。由于我们将批量训练我们的模型,我们还将创建数据迭代器,一次输出特定数量的样本:
# Create PyTorch iterators to use in training
train_iterator, valid_iterator, test_iterator = data.BucketIterator.splits(
(train_data, valid_data, test_data),
batch_size = BATCH_SIZE,
device = device)
BucketIterator 是 torchtext 中的一个模块,它经过专门优化,可以在为每个新时段生成新的混洗批次时,最大限度地减少所需的填充量。现在我们已经完成了文本预处理,所以是时候学习更多关于 CNN 的知识了。
回旋
卷积是应用于矩阵的滑动窗口函数,其实现特定的结果(例如,图像模糊、边缘检测。)滑动窗口被称为*内核、T2 滤波器、或特征检测器。*可视化显示了六个 3×3 核,它们将值与原始矩阵逐元素相乘,然后求和。为了获得完整的卷积,我们通过在整个矩阵上滑动滤波器来对每个元素进行卷积:
CNN 只是几层带有激活函数的卷积,就像 ReLU 一样,使得对非线性关系建模成为可能。通过应用这组点积,我们可以从图像中提取相关信息,从较浅层次的边缘开始,到在较深层次的神经网络上识别整个对象。与简单平坦化输入的传统神经网络不同,CNN 可以提取对图像数据特别有用的空间关系。但是文字呢?
用于 NLP 的 CNN
还记得我们上面讨论的单词嵌入吗?这就是他们发挥作用的地方。图像只是空间中的一些点,就像单词向量一样。通过用特定长度的数字向量表示每个单词,并将一堆单词堆叠在一起,我们得到了一个“图像”计算机视觉过滤器通常具有相同的宽度和高度,并在图像的局部滑动。在 NLP 中,我们通常使用滑过单词嵌入(矩阵行)的过滤器。因此,过滤器的宽度通常与单词嵌入的长度相同。高度变化,但一般从 1 到 5,对应不同的 n-gram。N-grams 只是一堆后续词。通过分析序列,我们可以更好地理解句子的意思。例如,单词“like”单独与双字组“don’t like”相比具有相反的意思;后者让我们更好地理解真正的意义。在某种程度上,通过分析 n 元语法,我们正在捕捉文本中的空间关系,这使得模型更容易理解情感。下面的图像总结了我们刚刚谈到的概念:
Source: Lopez et al. (2017) Link: https://arxiv.org/pdf/1703.03091.pdf
PyTorch 模型
现在让我们建立一个二元 CNN 分类器。我们将我们的模型建立在内置 PyTorch nn 的基础上。模块:
class CNN_Text(nn.Module):
''' Define network architecture and forward path. '''
def __init__(self, vocab_size,
vector_size, n_filters,
filter_sizes, output_dim,
dropout, pad_idx):
super().__init__()
# Create word embeddings from the input words
self.embedding = nn.Embedding(vocab_size, vector_size,
padding_idx = pad_idx)
# Specify convolutions with filters of different sizes (fs)
self.convs = nn.ModuleList([nn.Conv2d(in_channels = 1,
out_channels = n_filters,
kernel_size = (fs, vector_size))
for fs in filter_sizes])
# Add a fully connected layer for final predicitons
self.linear = nn.Linear(len(filter_sizes) \
* n_filters, output_dim)
# Drop some of the nodes to increase robustness in training
self.dropout = nn.Dropout(dropout)
def forward(self, text):
'''Forward path of the network.'''
# Get word embeddings and formt them for convolutions
embedded = self.embedding(text).unsqueeze(1)
# Perform convolutions and apply activation functions
conved = [F.relu(conv(embedded)).squeeze(3)
for conv in self.convs]
# Pooling layer to reduce dimensionality
pooled = [F.max_pool1d(conv, conv.shape[2]).squeeze(2)
for conv in conved]
# Dropout layer
cat = self.dropout(torch.cat(pooled, dim = 1))
return self.linear(cat)
在init
函数中,我们指定了不同的层类型:嵌入、卷积、下降和线性。所有这些层都集成到 PyTorch 中,非常易于使用。唯一棘手的部分是计算正确的维数。在线性图层的情况下,它将等于您使用的过滤器的数量(我使用 100,但您可以选择任何其他数字)乘以不同过滤器大小的数量(在我的情况下为 5。)我们可以把这个线性层的权重看作是从 500 个 n 元语法中的每一个“加权证据”。forward
函数指定这些层的应用顺序。请注意,我们也使用最大池层。max-pooling 背后的思想是,最大值是用于确定评论情绪的“最重要”特征,这对应于通过反向传播识别“最重要”的 n 元语法。最大池对于减少网络中的参数和计算的数量也是有用的。
指定网络架构后,让我们加载之前导入的预训练手套嵌入:
# Initialize weights with pre-trained embeddings
model.embedding.weight.data.copy_(TEXT.vocab.vectors)# Zero the initial weights of the UNKnown and padding tokens.
UNK_IDX = TEXT.vocab.stoi[TEXT.unk_token]# The string token used as padding. Default: “<pad>”.
PAD_IDX = TEXT.vocab.stoi[TEXT.pad_token]model.embedding.weight.data[UNK_IDX] = torch.zeros(EMBEDDING_DIM)
model.embedding.weight.data[PAD_IDX] = torch.zeros(EMBEDDING_DIM)
model = model.to(device)
该代码块的第二部分将未知向量(词汇表中不存在的向量)和填充向量(在输入大小小于最大过滤器的高度的情况下使用)设置为零。我们现在准备训练和评估我们的模型。
您可以在本笔记本中找到完整的培训和评估代码:
Link: https://gist.github.com/ritakurban/c9ebcbfa0be45952c99ccd199b57af3d
在训练模型之前,我们需要指定网络优化器和损失函数。Adam 和二元交叉熵是分类问题的常用选择。为了训练我们的模型,我们获得模型预测,使用损失函数计算它们的精确度,并在下一次运行之前通过网络反向传播以优化权重。我们在model.train()
模式下执行所有这些动作。为了评估模型,不要忘记打开model.eval()
模式,以确保我们没有用dropout
丢弃一半的节点(虽然在训练阶段提高了健壮性,但在评估期间会有伤害)。我们也不需要在评估阶段计算梯度,这样我们就可以借助torch.no_grad()
模式将其关闭。
在对模型进行了几个纪元的训练(使用 GPU 加速)后,我得到了以下损失和精度:
Losses and Accuracies
该图显示了过度拟合的迹象,因为训练损失和精度都在不断提高,而验证损失和精度却越来越差。为了避免使用过度拟合的模型,我们只在验证损失增加的情况下保存模型。在这种情况下,验证损失在第三个时期后最高。在训练循环中,这一部分如下所示:
if valid_loss < best_valid_loss:
best_valid_loss = valid_loss
torch.save(model.state_dict(), 'CNN-model.pt')
该模型在之前未见过的测试集上的表现相当不错:85.43%。最后,让我们使用 CNN-model 来预测一些极地评论的情绪。为此,我们需要编写一个函数,将用户输入符号化,并将其转换为张量。之后,我们使用刚刚训练的模型进行预测:
def sentiment(model, sentence, min_len = 5):
'''Predict user-defined review sentiment.'''
model.eval()
tokenized = [tok.text for tok in nlp.tokenizer(sentence)]
if len(tokenized) < min_len:
tokenized += ['<pad>'] * (min_len - len(tokenized))
# Map words to word embeddings
indexed = [TEXT.vocab.stoi[t] for t in tokenized]
tensor = torch.LongTensor(indexed).to(device)
tensor = tensor.unsqueeze(0)
# Get predicitons
prediction = torch.sigmoid(model(tensor))
return prediction.item()
在原始数据集中,我们有分别映射到 0 和 1 的标签“pos”和“negs”。让我们看看我们的模型在正面、负面和中性评论中的表现如何:
reviews = ['This is the best movie I have ever watched!',
'This is an okay movie',
'This was a waste of time! I hated this movie.']
scores = [sentiment(model, review) for review in reviews]
模型预测分别是 0.007,0.493,0.971,相当不错!让我们尝试一些更复杂的例子:
tricky_reviews = ['This is not the best movie I have ever watched!',
'Some would say it is an okay movie, but I found it terrific.',
'This was a waste of time! I did not like this movie.']
scores = [sentiment(model, review) for review in tricky_reviews]
scores
不幸的是,由于该模型已经在极地评论上训练过,它发现很难对棘手的陈述进行分类。例如,第一个棘手的评论得了 0.05 分,这是非常自信的“是”,尽管句子中存在否定。尝试使用不同的 n-gram,看看它们中的一些是否比其他的更重要,也许使用二元和三元模型会比我们使用的不同 n-gram 的组合表现得更好。
Table of reviews and their sentiment scores
结论
在这篇文章中,我们回顾了卷积的概念,并讨论了如何使用它们来处理文本。我们还学习了如何预处理来自 PyTorch 的数据集,并为情感分析建立了一个二元分类模型。尽管被狡猾的例子愚弄了,但模型表现得相当好。我希望你喜欢阅读这篇文章,如果你有任何问题,请随时联系我!
参考
布里茨博士(2015 年)。理解用于 NLP 的卷积神经网络。检索自:http://www . wild ml . com/2015/11/understanding-convolutionary-neural-networks-for-NLP/
洛佩兹,M. M .,&卡利塔,J. (2017)。深度学习在自然语言处理中的应用。 arXiv 预印本 arXiv:1703.03091 。检索自:【https://arxiv.org/pdf/1703.03091.pdf
Trevett,B. (2019)。卷积情感分析。检索自:https://github . com/bentrevett/py torch-opinion-analysis/blob/master/4% 20-% 20 convolatile % 20 opinion % 20 analysis . ipynb
CNN 与用于图像处理的全连接网络
介绍
本文的目的是提供一个理论视角来理解为什么(单层)细胞神经网络在图像处理方面比全连接网络更好。将使用线性代数(矩阵乘法、特征值和/或 PCA)和 sigmoid/tanh 函数的性质,尝试在全连接网络(逻辑回归)和 CNN 之间进行一对一(几乎)比较。最后,为了预测的目的,将检查滤波器大小和滤波图像中保留的信息量之间的权衡。为简单起见,我们假设如下:
- 全连接网络没有隐藏层(逻辑回归)
- 原始图像被标准化为像素值在 0 和 1 之间,或者被缩放为均值= 0,方差= 1
- Sigmoid/tanh 激活用于输入和卷积图像之间,尽管该参数适用于其他非线性激活函数,如 ReLU。避免使用 ReLU,因为如果图像经过缩放(平均值= 0,方差= 1)而不是归一化,它会破坏分析的严谨性
- 通道数=图像深度= 1 对于本文的大部分内容,将简要讨论通道数较高的模型
- 这个问题涉及到分类任务。因此,C > 1
- 除了激活之外没有非线性,也没有不可微性(如池化、除 1 之外的步幅、填充等。)
- 负对数似然损失函数用于训练两个网络
符号和记号
使用的符号有:
- x:二维输入图像的矩阵
- 用于将
原始图像映射到全连接网络中的输出的 W₁、b₁:权重矩阵和偏差项
过滤后的图像映射到 CNN 中的输出 - p:输出概率
- X₁:滤波图像
- x₁:滤波激活图像
关于符号需要注意的两个约定是:
- 尺寸写在{}之间
- 不同的维度由 x 分隔。例如:{n x C}表示二维“数组”
模型定义
全连接网络
FC1: Pre-ouptut layer
FC2: Estimated probability
卷积神经网络
C1: Filtered image
C2: Filtered-activated image
Activation functions
C3: Pre-output layer
C4: Estimated probability
数学
将 CNN 简化为一个全连接网络
假设滤波器是平方的,kₓ = 1,K(a,b) = 1。因此,X₁ = x。现在将使用归一化 x 的优点和 sigmoid/tanh 的便利性质。讨论如下:
sigmoid/tanh 的必需属性
Sigmoid activation as a function of input. Courtesy: ResearchGate article [1]
我们观察到,当输入量很小时,函数是线性的。由于输入图像被归一化或缩放,所有值 x 将位于 0 附近的小区域中,使得|x|
这表明过滤激活图像中的信息量非常接近原始图像中的信息量。过滤激活图像的所有像素都连接到输出层(完全连接)。
让我们假设我们学习了输入层完全连接到输出层的全连接网络的最优权重 W₁、b₁。我们可以直接获得给定 CNN 的权重,如 W₁(CNN) = W₁/k 重排为矩阵,b₁(CNN) = b₁.因此,对于 kₓ = 1 且 K(1,1) = 1 的方形滤波器,全连接网络和 CNN 将表现(几乎)相同。
由于双曲正切函数是一个重新标度的 sigmoid 函数,因此可以认为同样的性质也适用于双曲正切函数。这也可以在下图中观察到:
tanh activation as a function of input. Courtesy: Wolfram MathWorld [2]
滤波器——最差情况
让我们考虑一个正方形图像上的正方形滤波器,其中 kₓ = nₓ,对于所有 a,b,K(a,b) = 1。首先,该滤波器将每个图像映射到一个值(滤波后的图像),然后该值映射到 c 个输出。因此,过滤后的图像包含的信息(信息瓶颈)比输出图层少-任何少于 C 个像素的过滤后图像都将成为瓶颈。第二,该过滤器将每个图像映射到等于图像值总和的单个像素中。这显然包含非常少的关于原始图像的信息。让我们考虑 MNIST 的例子来理解为什么:考虑具有真实标签‘2’和‘5’的图像。这些图像的值的总和不会相差太多,然而网络应该使用该信息学习一个清晰的边界。
放松最坏情况第 1 部分:滤波器权重
让我们考虑正方形图像上的正方形滤波器,其中 kₓ = nₓ,但是 k 中不是所有的值都相等。这允许 k 中的变化,使得重要性给予某些像素或区域(将所有其他权重设置为常数,并且仅改变这些权重)。通过改变 K,我们可以发现图像中有助于分类的区域。例如,在 MNIST,假设所有的数字都按照一个通用的模板居中并且写得很好,即使只有一个值被映射到 C 输出,这也可以在类之间创建合理的分离。考虑这种情况类似于判别分析,其中单个值(判别函数)可以分离两个或多个类。
放松最坏情况第 2 部分:滤波器宽度
让我们考虑正方形图像上的正方形滤波器,对于所有的 a,b,K(a,b) = 1,但是 kₓ ≠ nₓ.例如,让我们考虑 kₓ = nₓ-1.原始和过滤后的图像如下所示:
Original image
Filtered image
请注意,过滤后的图像总和仅在第一行、第一列、最后一行和最后一列中包含一次元素。所有其他元素出现两次。假设滤波图像中的值很小,因为原始图像被归一化或缩放,对于小值 k,激活的滤波图像可以近似为滤波图像的 k 倍。在诸如矩阵乘法(具有权重矩阵)的线性运算下,当 k 为非零时,kx₁的信息量与 x₁的信息量相同(此处为真,因为 sigmoid/tanh 的斜率在原点附近为非零)。因此,过滤激活图像包含(大约)与过滤图像相同数量的信息*(为了便于理解,写得非常松散,因为【费希尔】‘信息’是得分函数的方差,它与 RV 的方差有关。这种说法的更好版本是:“缩放/归一化的输入图像和缩放/归一化的滤波图像将具有大约相同的信息量”)。
假设原始图像具有非冗余像素和非冗余像素排列,通过应用(nₓ-1,nₓ-1)滤波器,图像的列间距从(nₓnₓ)减少到(2,2)。这导致信息丢失,但对于 K(a,b) = 1,它保证比(nₓ,nₓ)滤波器保留更多的信息。随着过滤器宽度的减小,过滤(因此,过滤激活)图像中保留的信息量增加。它在 kₓ = 1 时达到最大值。
在诸如 MNIST 的实际情况下,边缘附近的大多数像素是冗余的。因此,通过在没有数字信息的边缘附近应用大小为~宽度的块的过滤器,几乎可以保留所有信息。
把东西放在一起
CNN 的一个特殊属性是在图像的所有区域应用相同的滤波器。这就是所谓的重量共享。模型中的参数总数= (kₓ * kₓ) + (nₓ-kₓ+1)*(nₓ-kₓ+1)*C.
- 较大的过滤器导致较小的过滤激活图像,这导致通过全连接层传递到输出层的信息量较少。这导致低信噪比、较高的偏置,但由于全连接层中的参数数量减少,因此降低了过拟合。这是一个高偏差、低方差的例子。
- 较小的过滤器导致较大的过滤激活图像,这导致更大量的信息通过全连接层传递到输出层。这导致高信噪比、较低的偏置,但可能导致过拟合,因为全连接层中的参数数量增加了。这是一个低偏差、高方差的例子。
已知 K(a,b) = 1,kₓ=1 表现(几乎)以及全连通网络。通过反向传播(链式法则)和 SGD 调整 kₓ ≠ 1 的 K(a,b ),保证模型在训练集上表现更好。当用一组不同的超参数(kₓ).)训练时,它也趋向于具有比全连接网络更好的偏差-方差特性
总结
kₓ = 1 且 K(1,1) = 1 的 CNN 可以匹配全连接网络的性能。对于 kₓ = nₓ和 K(a,b) = 1,滤波激活图像的表示能力最小。因此,通过调整超参数 kₓ,我们可以控制滤波激活图像中保留的信息量。此外,通过将 K 调整为不同于 1 的值,我们可以聚焦于图像的不同部分。通过调整超参数 kₓ和学习参数 k,CNN 保证具有更好的偏差-方差特性,其下限性能等于全连接网络的性能。这可以通过具有多个通道来进一步改善。
扩展上述讨论,可以认为,如果 CNN 具有相同数量的具有相同/相似结构(每层中的神经元数量)的隐藏层,则 CNN 将优于全连接网络。
然而,这种比较就像把苹果和橘子相比较。适当的比较是将全连接神经网络与具有单个卷积+全连接层的 CNN 进行比较。将具有 1 个隐藏层的全连接神经网络与具有单个卷积+全连接层的 CNN 进行比较是更公平的。
实践中的 MNIST 数据集:逻辑回归模型学习每个数字的模板。这实现了很好的准确性,但是并不好,因为模板可能不能很好地概括。具有完全连接的网络的 CNN 学习适当的内核,并且过滤的图像较少基于模板。与 CNN 相比,具有 1 个隐藏层的全连接网络显示出较少的基于模板的迹象。
参考
谭:【http://mathworld.wolfram.com/HyperbolicTangent.html】T4
用于目标检测的 COCO 和 Pascal VOC 数据格式
理解计算机视觉的注释数据格式
在本文中,我们将了解两种流行的数据格式:COCO 数据格式和 Pascal VOC 数据格式。这些数据格式用于注释在用于计算机视觉的数据集中发现的对象。我们将特别关注对象检测的注释
计算机视觉中最重要的任务之一是标记数据。有几个工具可供您加载图像,使用每个实例的分段来标记对象。这有助于使用边界框进行精确的对象定位,或者使用多边形进行遮罩。该信息存储在注释文件中。
注释文件可以是 COCO 或 Pascal VOC 数据格式。
COCO 是什么?
COCO 是用于对象检测、分割和字幕数据集的大规模图像与上下文中的公共对象(COCO)。COCO 拥有 80 个对象类别的 150 万个对象实例
COCO 有 5 种注释类型用于
COCO 将注释存储在 JSON 文件中。让我们看看存储边界框注释细节的 JSON 格式。这将有助于使用 COCO 格式创建您自己的数据集。
JSON 注释文件的基本构建块是
- 信息:包含数据集的高级信息。
- 许可证:包含适用于数据集中图像的图像许可证列表。
- 类别:包含类别列表。类别可以属于一个超级类别
- 图像:包含数据集中的所有图像信息,没有边界框或分割信息。图像 id 需要是唯一的
- 注释:数据集中每幅图像的单个对象注释列表
Sample COCO JSON format
我们可以为训练、测试和验证数据集创建单独 JSON 文件。
让我们深入了解每一部分
信息:
提供有关数据集的信息。
template and example for info section of the JSON for COCO
许可证:
我们可以提供数据集中使用的不同图像许可证的列表。
template and example for Licenses section of the JSON for COCO
类别:
每个类别 id 必须是唯一的。一个类别可以属于一个超类别。举个例子,如果我们有数据集来识别花和水果。花卉将是超级类别,玫瑰、百合、郁金香将是我们想要检测的花卉的名称。
template and example for Categories section of the JSON for COCO
图像:
包含数据集中所有图像的列表。图像 id 应该是唯一的。flickr_url、coco_url 和 date_captured 是可选的
template and example for images section of the json for COCO
注释:
包含数据集中每个图像的每个单独对象注释的列表。这是包含用于对象检测的边界框输出或对象分割的部分
如果一幅图像有 4 个我们想要检测的对象,那么我们将有所有 4 个对象的注释。
如果整个数据集由 150 幅图像组成,总共有 200 个对象,那么我们将有 200 个注释。
分割包含分割遮罩的每个对象实例周围多边形顶点的 x 和 y 坐标。
面积是包围盒的面积。它是一个像素值
iscrowd :如果我们有一个单一的对象分割,那么 iscrowd 设置为零。对于图像中出现的对象集合,我们设置 iscrowd=1,在这种情况下使用 RLE。
RLE 是游程编码。当 iscrowd=1 时,我们在分段部分添加属性计数和大小。这是下例中的第二个分段
如果被遮挡,单个对象(iscrowd=0)可能需要多个多边形。
imageid :它是包含我们为其指定注释的对象的图像的 id。imageid 对应于 image 部分中的 imageid
bbox:COCO 中的包围盒是左上的 x 和 y 坐标以及高度和宽度。Pascal VOC 边界框是矩形左上角的 x 和 y 坐标以及右下角的 x 和 y 坐标。
COCO 包围盒:(x-左上,y-左上,宽度,高度 )
Pascal VOC 边界框:(x-左上,y-左上,x-右下,y-右下 )
类别:这是我们之前在类别部分指定的对象类别
id :标注的唯一 id
template and example for annotations section of the JSON for COCO
什么是游程编码(RLE)?
RLE 是一种压缩方法,其工作原理是用重复的次数替换重复的值。
例如,0 11 0111 00 将变成 1 2 1 3 2。
COCO 数据格式为每个对象实例提供分段掩码,如上文分段部分所示。这就产生了效率问题
- 紧凑地存放面罩
- 以有效地执行掩码计算。
我们使用游程编码(RLE)方案来解决这两个问题。
RLE 表示的大小与掩模的边界像素的数量成比例。面积、并集或交集等运算将在 RLE 上高效地计算。
Pascal 可视对象类(VOC)
Pascal VOC 为目标检测提供标准化的图像数据集
COCO 和 Pacal VOC 数据格式之间的差异将有助于快速理解这两种数据格式
- Pascal VOC 是一个 XML 文件,不像 COCO 有一个 JSON 文件。
- 在 Pascal VOC 中,我们为数据集中的每个图像创建一个文件。在 COCO 中,我们每个人都有一个文件,用于整个数据集的训练、测试和验证。
- Pascal VOC 和 COCO 数据格式中的边界框是不同的
COCO 包围盒:(x-左上,y-左上,宽度,高度 )
Pascal VOC 包围盒:(xmin-左上,ymin-左上,xmax-右下,ymax-右下 )
Sample Pascal VOC
Pascal VOC 的一些关键标签解释如下
文件夹:
包含图像的文件夹
文件名:
文件夹中存在的物理文件的名称
尺寸:
包含图像的宽度、高度和深度。如果图像是黑白的,那么深度将是 1。对于彩色图像,深度将是 3
对象:
包含对象详细信息。如果您有多个注释,那么对象标签及其内容会重复。对象标签的组件包括
- 名字
- 姿态
- 缩短了的
- 困难的
- bndbox
名称:
这是我们试图识别的对象的名称
截断的:
指示为对象指定的边界框不符合对象的全图。例如,如果一个对象在图像中部分可见,那么我们将 truncated 设置为 1。如果对象完全可见,则将 truncated 设置为 0
困难:
当对象被认为难以识别时,该对象被标记为困难的。如果对象很难识别,那么我们将困难设置为 1,否则设置为 0
边界框:
指定图像中可见对象范围的轴对齐矩形。
本文应该有助于理解计算机视觉中使用的两种流行数据格式的细节
参考资料:
http://cocodataset.org/#download
编辑描述
cocodataset.org](http://cocodataset.org/#format-data) [## 如何在数据集 json 文件中编写对象分段?问题#111 cocodataset/cocoapi
此时您不能执行该操作。您已使用另一个标签页或窗口登录。您已在另一个选项卡中注销,或者…
github.com](https://github.com/cocodataset/cocoapi/issues/111)
https://pjreddie.com/media/files/VOC2012_doc.pdf
https://arxiv.org/pdf/1405.0312.pdf
波士顿安全饮用水工程规范
理解数据意味着从意想不到的地方学习
我第一次参加波士顿黑客之夜是在去年 12 月。当时我正在参加一个密集的数据科学项目。那周我们有多个实验要做(这是常态),我的几个同学被做任何额外的事情的想法吓了一跳。但是,在看过波士顿项目的一些代码后,我渴望做出贡献,并与其他对使用技术解决公民和社会问题感兴趣的人建立联系。
您可以在波士顿的当前项目页面的代码上看到关于以下项目的附加信息:
- 波士顿信息语音应用:回答波士顿市政服务问题的 Alexa 技能。目前支持提供地址和要求垃圾/回收提货日。
- Community Connect :一个健康资源网络应用,收集关于促进健康生活方式选择的企业和组织的信息。
- MuckRock :归档、跟踪和共享公共记录请求。
- 安全饮用水项目:预测安全饮用水违规行为的探索性数据项目。它使用 EPA 的安全饮用水信息系统和 EnviroFacts API。
- 移民服务地图:一款网络应用,旨在帮助移民服务提供商创建个人资料,更新信息,并快速与客户分享。
- **意外收入意识项目:**一种帮助受社会保障意外收入消除计划(WEP)影响的退休人员的工具,该计划可以将某些公务员的社会保障福利减少多达 50%。这个工具将帮助受影响的工人更好地规划退休和自我主张与社会保障管理局。
- 当你步履沉重时,你会在日常生活中捡垃圾……慢跑、徒步旅行或只是在街上散步。Plogalong 帮助您跟踪您的 plogs,与附近的 ploggers 联系,获得徽章,并获得当地折扣。
这是一个艰难的选择,但我最终加入了安全饮用水项目,因为他们专门寻找具有数据科学技能的人。我也觉得和这个问题有关联。
一个关于学校的故事
几年前,我参加了当地一所高中同伴领导小组的第一次会议。在我们等待大家入座的时候,我问最近的饮水机在哪里。几个学生扬起了眉毛。
“为什么?”
“这样我就可以装满我的水瓶了.”
“就在大厅那头。但你不想那样。”
“你是认真的吗?”
“是啊,我们不喝这里的水。”
房间里的每个人都点头表示同意。
在会议期间,学生们完成了一项活动,他们在活动中确定了他们所关注的学校和社区中的问题。当我们结束一场热烈的讨论时,我不得不抛出最后一个问题:“没人说过水。你们有谁在乎自己学校不能喝水?”
他们都耸耸肩。这是一种常态,不值一提。
同一周,马萨诸塞州州长查理·贝克宣布拨款 200 万美元帮助公立学校检测饮用水中的铅和铜。
仅仅一年多之后,2017 年 5 月,这篇文章出现在我的 feed 中:
[## 质量测试。学校发现超过一半的饮用水中含有高浓度的铅和铜
波士顿(哥伦比亚广播公司)——上周对马萨诸塞州 1000 多所学校的测试发现,超过一半的学校有饮用水…
boston.cbslocal.com](https://boston.cbslocal.com/2017/05/02/school-lead-testing-drinking-water-massachusetts/)
测试的总结结果证实了我合作的两所高中都“领先于行动水平”
理解数据
这让我们回到波士顿的安全饮用水项目。目标是预测饮用水中基于健康的违规行为。我们从环保局的安全饮用水信息系统 (SDWIS)的数据开始。
对我来说,理解数据不仅仅意味着知道 SDWIS 数据库中包含什么信息(尽管我们终于有了一个集中的数据字典!).这也是为了理解这些信息实际上告诉我们什么,并确定数据库中没有包括的其他有助于水安全的因素。
惊人的联系
虽然我一直在积极寻找信息,以加深对安全饮用水相关问题的理解,但我的一些知识却来自意想不到的地方。
弗林特的艾
1 月初,这篇文章出现在我的收件箱中——不是因为我对水资源问题的跟踪,而是因为我对机器学习的兴趣:
一个机器学习模型显示了有希望的结果,但是城市官员和他们的工程承包商放弃了它…
www.theatlantic.com](https://www.theatlantic.com/technology/archive/2019/01/how-machine-learning-found-flints-lead-pipes/578692/)
TL;这篇文章的博士版本是,一个团队创建了一个机器学习模型,成功预测了弗林特的哪些家庭可能有铅管。该市与一家国有公司 AECOM 签订了合同,以加快工程进度。然后:
AECOM 抛弃了指导挖掘的机器学习模型的预测。面对一些居民的政治压力,[弗林特市长卡伦]韦弗要求该公司挖掘整个城市的选区和选定街区的每一所房屋,而不是挑选出可能有铅的房屋,因为年龄、财产类型或其他特征可能与管道相关。
在对项目管理进行了数百万美元的投资后,弗林特成千上万的人仍然拥有铅管住宅,而以前的项目可能已经找到并替换了它们。
海平面正在上升
弗林特的文章发表后的一周,我在麻省理工学院媒体实验室参加了“黑人生活数据 II”会议。
从那以后,最让我念念不忘的是*“海在上涨,人也在上涨”:数据、灾难&集体力量*。
小组成员 Valencia Gunder、Denice Ross、Lisa Rice 和 Bina Venkataraman 分享了他们的工作并回答了问题:“黑人社区使用社交媒体、数据和技术为下一场灾难做准备的方式是什么?为了预防?在这些快速反应时刻和气候正义运动中,数据科学家和工程师的角色是什么?”
他们谈到气候变化加剧了高房价和流离失所的问题。巴伦西亚·贡德开始了她的故事:
当你在南佛罗里达工作时,气候变化和恢复力总是谈话的一部分,即使是以最不正式的方式。小时候,我的祖父告诉我,“他们会偷走我们的社区,因为那里不发洪水。”直到我长大了,我才理解他。
Map created by Hugh Gladman that plots owners with 15 or more pieces of land in Little Haiti to the height above sea level of their parcels. (Source: The New Tropic)
其他人也谈到洪水和社区不平等之间的联系。“黑人生活的数据”YouTube 频道有一个完整的小组的记录,以及会议的其他环节,可供查看。
农田径流
在完成数据科学项目后不久,我与一个人进行了交谈,他对一个亲戚进行了离题的评论,这个亲戚使用人工智能来研究奶牛的食物如何影响它们的粪便,从而由于径流而影响水系统。他挥挥手表示拒绝:“但你可能对此不感兴趣。”
我解释说我对这个话题很感兴趣。这是一个促成因素,我们在安全饮水小组中还没有讨论过。我做了一些调查,以了解更多关于粪肥和其他农田径流是如何污染水源的。
2009 年《纽约时报》的一篇文章考虑了威斯康星州的问题,在那里“仅一个镇就有超过 30%的水井违反了基本健康标准”:
农场动物的排泄物据说是饮用水中污染物的一个来源。在一次市政厅会议上,愤怒的房主对牛奶场主大喊大叫,其中一些人被认为是镇上最富有和最有权势的人。
它继续解释说,即使当地的环境机构没有被过度征税,“一个强大的农业游说团体已经阻止了以前在 Capital[sicHill 的环境努力。即使州立法机关采取了行动,也经常遇到意想不到的困难。”
Screenshot of EWG’s interactive map of drinking water in rural communities threatened by farm pollution. (Source: EWG)
不同的政治和经济利益在起作用。虽然 Valencia Gunder 谈到了洪水和气候变化导致的佛罗里达州的流离失所,但像上面这样的地图(也使用了 SDWIS 的数据,以及来自美国农业部农业普查的数据)表明了额外的担忧。在已经受到农业污染威胁的地区,洪水的增加增加了水系统受到污染的风险。
有毒化学场所
在本月早些时候哈佛的下一次数据可视化活动中,纽约时报的图形编辑布莱基·米格里奥齐分享了他的一些气候变化可视化。其中一个特别引起了我的注意:一张地图显示了全国 2500 个位于洪水易发区的化学场所。
在美国每个州的洪水易发区,有超过 2500 个处理有毒化学物质的场所,纽约…
www.nytimes.com](https://www.nytimes.com/interactive/2018/02/06/climate/flood-toxic-chemicals.html)
这张地图是利用美国环保署有毒物质释放清单和美国联邦应急管理局国家洪水危险层的数据绘制的。
与农场径流带来的问题类似,海平面上升导致洪水泛滥的地区的化学工厂也面临着污染水系统的更高风险,以及一系列其他问题。
被毒害的城市
几天后,我和另一名安全饮水志愿者去见记者安娜·克拉克谈论她的书被污染的城市:弗林特的水和美国城市悲剧。当然,在这种情况下,我期望了解更多关于影响饮用水的因素,但仍然觉得值得在这里包括。
在简要介绍了该市从五大湖水转向弗林特河水,以及随之而来的铅、大肠杆菌和军团病问题后,克拉克问道,“首先是什么让一个城市如此脆弱?”
她的回答:“你得往回走几代。”
从那里,她的故事开始于 20 世纪 60 年代,“当时弗林特是北方种族隔离最严重的城市,也是全国第三大种族隔离城市。”它包括通用汽车公司的影响、社区组织的历史、争取公平住房的斗争、去工业化和应急管理法。
关于这方面的图文并茂的文章,请看克拉克与平面艺术家乔希·克莱默的合作:
一篇插画的原创文章。关于来源的说明:所有归功于历史人物的东西都直接来自于…
splinternews.com](https://splinternews.com/an-equal-opportunity-lie-how-housing-discrimination-le-1820482045)
对我来说,克拉克在演讲后的谈话中提出的两个评论很突出:
环境问题总是以不承认有罪的和解告终。
和
这不仅仅是一个技术问题。这种事发生在弗林特是有原因的,不会发生在安阿伯。
A copy of The Poisoned City, signed “For #water, Anna Clark.”
约翰,另一个参加讲座的安全饮水志愿者,拿到了一本《被污染的城市》。克拉克把它签到了#water(我们组对波士顿 Slack 频道的代号),它现在正在被读取并通过组传递。
巴黎圣母院
与此同时,这个推特时刻今天早上在我的收件箱里等待着:
[## 白宫对圣母大学的援助重新开启了关于波多黎各和弗林特需求的对话
一些人想起了白宫提供的资源,波多黎各和弗林特继续需要这些资源…
twitter.com](https://twitter.com/i/events/1118477687712935936)
这再次提醒我们不同的利益、价值观、优先事项和资源分配会如何影响饮用水(以及其他许多东西)。
邀请函
这些是我在试图理解我们正在使用的数据时建立的一些联系。一如既往,我很高兴听到对此的任何想法。
如果您对安全饮用水项目的其他数据、研究或方法有建议,请分享!您还可以查看安全饮用水项目 GitHub repo ,并加入# Water channel onCode for Boston Slack。
如果你在波士顿地区,并且对参与这个或另一个波士顿代码项目感兴趣,在波士顿代码网站上了解更多,并参加每周一次的黑客之夜。
感谢阅读!想法、问题和反馈总是很受欢迎。
减少分支、机器人感知和圈复杂度
学习最佳实践的代码形
source: Steve Johnson via pexels
这是一个快速代码形来说明“减少分支”原则,并在 5 分钟内激起你对机器人感应的兴趣。
当我们创建一个机器人代理时,天真的解决方案是首先假设机器人有一致的可能性去“任何地方”在数学上,这被建模为机器人将向任何方向移动的等概率。为了简单起见,让我们将其限制在一维的情况下,假设我们的机器人只能在直线 1-D / X 轴上行进并访问 5 个门。
如果有 5 个门,并且所有概率都是均匀分布的,那么机器人位置的初始向量表示是 1 / 5 = 0.2。
所以,假设给你一个向量,代表你的统一的,简单的概率:
probs = [0.2, 0.2, 0.2, 0.2, 0.2]
现在,机器人感知它们的环境,这应该与它们的目标目的地一致。比方说,我们告诉我们的机器人,他只能旅行到一个“粉红色的门”,而不是蓝色的门。"
如果我们的机器人感觉到他在一扇粉色的门旁,他的传感器会说有 0.90 的概率他确实在一扇粉色的门旁。
因此,让我们给自己一些额外的变量,代表一个成功的“击中”失败的“错过”,以及一个变量,代表我们的环境或门。
doors = [‘pink’, ‘blue’, ‘blue’, ‘blue’, ‘pink’]T = ‘pink’pHit = 0.90pMiss = 0.10
你的目标是为我们的机器人编写一个感知函数,评估我们的目标 T 是否与它所在的门匹配,并根据它的均匀分布乘以它感知目标门的概率。
下面,我用 Python 3 提供了两个解决方案,一个是简单的解决方案,另一个抓住了上面的“代码形”本质。一个是天真的,另一个可以说是稍微好一点的。
天真的实现
def sense(probs, T): afterSense=[] for i in range(len(probs)): if doors[i] == T: afterSense.append(probs[i]*pHit) else: afterSense.append(probs[i]*pMiss) return afterSense
更好的实现
def sense(probs, T): afterSense = [] for i in range(len(probs)): hit = (T == doors[i]) afterSense.append(probs[i]*(hit*pHit + (1-hit) *pMiss)) return afterSense
在更好的实现中,您将自己从执行庞大的 4 行 if/then 评估中解放出来。相反,您可以在我们的代码中生成一个独立的、线性的路径,它可以被遍历并折叠成一行。
afterSense.append(probs[i]*(hit*pHit + (1-hit) *pMiss))
这是因为当我们评估:
hits = (T == doors[i])
存储在 hits 中的值将返回布尔值真(1)或布尔值假(0)。这符合一般的“良好的编码风格”原则,通常被称为减少或最小化分支。
最终,目标是限制圈复杂度的水平,圈复杂度是另一个度量“分支”或线性独立路径数量的指标。
如果你是视觉生物,也许这个图会对你有帮助。
Image adopted from McCabe, T.J., “A complexity measure”, IEEE Trans. on Software Engineering, SE-2(4), pp.308–320(1976)
希望这对其他开发者和好奇的人有所帮助!欢迎给我发信息或在下面评论,我随时欢迎反馈。
道德准则—人工智能和人力资源分析
昨晚,我看了一部由剑桥分析公司(Cambridge Analytica)发起的关于信息战的纪录片(关于网飞的“大黑客”)。克里斯托弗·怀利(Christopher Wylie)是一名前数据科学家,最终成为告密者,他在收集数百万脸书用户的个人数据方面发挥了作用,这些用户最终成为外国特工政治广告的目标。对于数据科学家来说,没有什么比访问大型数据集更令人兴奋的了。我们都想用这些数据以积极的方式改变世界。但是,如果我们无意中造成伤害,而不是好处呢?对于任何数据科学家来说,这都是一个关键问题,它同样适用于人员分析——人工智能或数据科学在人力资源中的应用。
以道德问题为导向
法律和道德问题交织在一起。法律在某种程度上是社会道德标准的体现。作为一个社会,我们认为公平公正地对待他人是道德的,因此有法律来防止负面影响。作为一个社会,我们认为人们应该被允许在公众视线之外做许多事情,因此我们有隐私法。
然而,法律并不强制每一种道德行为,当道德标准仍有疑问时,法律必然落后。
第七章禁止基于两个理由的就业歧视——“明显的歧视意图”和“不同的影响”。但是,法律上有先例允许组织产生不同的影响,如果这种影响来自业务需求,特别是在似乎没有歧视意图的情况下。这是一个漏洞,一旦数据科学和人工智能进入画面,它可能会取消所有的 Title VII。组织可以根据最终产生不同影响的数据做出人事决策,甚至不包括种族或性别等明显敏感的变量。算法可能会在有偏见的历史数据中找到群体的统计代理,并基于它们进行歧视——而所有负责的组织都声称业务需求和歧视是无辜的。
目前还不清楚法律将如何对待这种“黑箱”算法,所以我们这些从事人员分析的人有时会处于蛮荒的西部,法律不确定,任何事情都可能发生。当法律模棱两可时,我们需要依靠自己的道德指南针。
那么,人物分析结果和产品中最大的道德问题是什么?
- 由于数据中的偏差,决策容易受到偏差的影响吗?
- 对更多数据的渴望和个人隐私之间的平衡是什么?
- 我们应该如何对算法造成的潜在错误负责(无论它们是否被“正确”编程)?
- 对于部分受算法决策影响的人,应该传达什么信息?
- 求职者和员工何时可以只与机器交互?什么时候应该联系真实的人?
人员分析的道德准则
计算机械协会至少从 1992 年起就有了道德规范,美国图书馆协会从 1939 年起就有了关于信息用户的道德规范,远远早于主流计算。就像医生们宣读希波克拉底誓言一样,这些其他职业也需要一套道德准则,因为他们的工作可能会对真实的人产生潜在的影响。
我相信我们这些从事人员分析的人也需要一套道德准则来指导我们的工作。首先,这是我自己的草稿,它受到了其他职业道德准则的启发:
- 我会时刻注意隐私和安全。
- 我将向我的客户公开我的方法中的假设和限制。
- 我将鼓励组织客户对他们的利益相关者保持透明。
- 我将仅出于收集数据的目的使用数据。
- 如果我或我的组织客户希望将数据用于原始目的之外的目的,我们将从数据收集对象处获得明确许可。
- 我将努力沟通,使我提供的信息更难被滥用。
- 我将努力使用可解释的算法,特别是当它们有可能影响个人的选择、表现和职业发展决策时。
- 我将清楚、坦率地说明数据分析和技术能解决什么,不能解决什么。
- 我将告知我的客户在使用任何形式的自动化时的权衡。
- 我将警惕潜在的不良偏见,即使在法律允许的情况下。
本《道德准则》涵盖五个类别— 数据隐私、目的驱动型使用、透明度、限制可见性、和反偏见行动。对我来说,这是一个开始。我将继续改进这些代码,因为我从人员分析领域的其他专业人士那里学到了很多东西。
代码:要查看的新数据
the source — https://fossbytes.com/microsoft-ai-system-deepcoder/
脸书、汽车和波音有什么共同点?它们都运行在超过 2000 万行代码的源代码上。
六年前,马克·安德森说“软件正在吞噬世界”,看看大卫·麦坎多斯visualizating我们就能明白这有多真实。我们被源代码及其日益增加的复杂性和挑战所淹没:影子 IT、缺乏文档、语言和框架异构性、缺乏代码历史的可见性、没有维护代码的简单方法…
The size of code bases in different systems
就像网络上的大量数据一样,支持大数据应用程序,现在大型程序库(例如 GitHub、Bitbucket 中的开源代码)支持一种新的应用程序:“大代码”。我们已经积累了数十亿字节的开放源代码数据,但很少有人尝试充分利用这些封存在内部的知识。代码是要看的新数据!
对源代码使用机器学习意味着从现有代码中自动学习,以便解决诸如预测程序错误、预测程序行为、预测标识符名称或自动创建新代码等任务。这种新方法打开了软件和代码开发方式中极其令人兴奋的机会之门。
源代码上的机器学习:是什么?
源代码上的机器学习(#MLonCode)是一个新兴的令人兴奋的研究领域,它位于深度学习、自然语言处理、软件工程和编程语言的十字路口。
在机器学习领域,源代码分析目前不如图像或自然语言分析重要。因此,对于使用源代码作为预测的数据源,还没有成熟的标准技术。我们站在一个前沿技术领域,仍然需要大量的研究。
处理代码数据
机器学习是应用于数据的数学算法:因此,任何输入数据都必须有一个数学表示。例如,当你想处理一幅图像时,你必须把它转换成一个矩阵。这很容易描述:图像是像素的矩阵,像素是用来描述颜色的数字阵列。
当涉及到编码时,挑战就有点棘手了。代码包含不同层次的理解:
- 语义层:写的是什么
- 结构层次:它是如何写的
- 图流级别:代码的每个部分如何与代码的其余部分交互
源代码的意图和意义依赖于这三个层次的理解。
任何应用于源代码的机器学习技术都应该确保这三个理解层次的数学嵌入。嵌入的质量将影响模型的质量
(这个话题会在后面的文章中详细介绍)
代码上的机器学习:用例
自动测试您的代码
许多软件开发过程必须覆盖大量的单元和集成测试用例,这需要很长(很长,很长……)的时间来完全实现。作为开发人员,尤其是在测试中,持续集成(CI)涉及到测试用例的优先排序、选择以及每个周期的执行。
如果对提交的代码变更的影响存在不确定性,或者如果代码和测试之间的可追溯性链接不可用,那么选择最有希望的测试用例来检测 bug 是很困难的。
今天对代码的自动理解,可以帮助你根据测试用例的持续时间、上次执行和失败历史来区分它们的优先级。在不断变化的环境中,一些算法如 Retecs 方法学会对容易出错的测试用例进行优先排序。
现实世界的例子:网飞的工程师们运行了一系列测试和基准测试,从多个维度验证这项服务,包括音视频播放质量、许可证处理、加密、安全性……所有这些导致了大量的测试案例,其中大部分是自动化的,需要执行这些测试来验证运行网飞的设备的功能。为了加快测试过程,网飞工程师使用了sRetecs 工艺。当针对设备运行持续集成时,它可以帮助他们从成千上万个可用的测试用例中选择最有希望的测试子集,或者推荐一组针对设备执行的测试用例,这将增加设备实时失败的概率。
代码建议和完成
多年来,开发人员就像装配线上的工人一样,一遍又一遍地编写同一行代码来解决同一类问题。有多少次我在寻找解决某个具体问题的方法,却发现自己每六个月都在寻找同样的答案!(而且我知道……不止我一个人在)。
工程师在处理问题时,通常会寻找一种已经实现的方法来解决问题。多年来,已经提出了许多代码搜索工具和平台(保佑你 StackOverflow)来帮助开发人员。通常的方法通常将源代码视为文本文档,并利用信息检索模型来检索与给定查询匹配的相关代码片段。他们缺乏对查询和源代码语义的深刻理解。
今天对代码的机器学习使得能够在问题域中运行语义相似性搜索,而不是在解决方案域中搜索。
使用最多的技术是 Code2Vec 。比如著名的自然语言处理例子:
vec(“man”)-vec(“woman”) = vec(“king”)-vec(“queen”)
Code2Vec 模型学习与源代码相关的类比,例如:
vec("receive")-vec("send") = vec("downlaod")-vec("upload")
*现实世界的例子:*脸书今年发布了 Aroma ,这是一个代码到代码的搜索和推荐工具,它使用机器学习(ML)来使从大型代码库中获得洞察力的过程变得更加容易。让我们记住脸书,有超过 2B 行的代码…
代码审查
确定程序的正确性需要对程序的预期行为有精确的理解,并有一种方法以适合自动化检查的形式明确地传达这种理解。
今天的代码审查工具缺乏对代码理解的深度,这会导致不愉快的情况(例如高测试覆盖率,同时有一个不工作的程序或高水平的代码文档,而每个注释都是过时的)。
对代码的机器学习为(深入)理解代码的意图并对其进行分析打开了一扇新的大门。这就像将二维图像与三维图像进行比较。是的,当前的代码审查工具确实提供了对代码的洞察,但与 ML 驱动的工具所能做的相比,这根本不算什么。
真实世界示例 : Autosoft 发布了第一个版本的代码审查工具,该工具评估代码及其注释的一致性,如果注释与代码有偏差,则自动建议更新。下一个版本将会比较单元测试和代码的有用性。当审查代码时,这使开发人员能够对如何提高任何代码的长期可读性和可维护性有真正可行的见解:这对我们来说很关键。
程序归纳和综合
这将是计算机科学自 200 年前创立以来的圣杯:获得一个完全自动化的编程系统。在计算机科学中,程序综合是自动构造满足给定高级规范的程序的任务。我们目前处于指导性编程的世界。作为开发人员,当我们必须解决一个复杂的问题时,我们将它分解成更小的问题,并编写解决这些问题的代码(我们讨论首要原则)。程序合成的工作方式正好相反:我们给计算机提供一个复杂的问题(我们想要的),把如何解决它的细节留给计算机。
我们现在还不能确定(请等一等,发言先生)但是最近研究人员和公司方面都取得了巨大的进步。今天我们可以谈谈增强编程。
现实世界的例子:
Pix2Code :虽然传统上前端开发人员的任务是将设计师的工作从原始的图形用户界面模型转化为实际的源代码,但这种趋势可能很快就会成为过去。他们的代码检测实体模型中的形状,解释它们的含义(段落、标题、图像……)并生成相关代码。
结论
让我们的想象力更进一步。如果 ML 生成的代码至少和最好的人类程序员可能产生的代码一样好,那可能会加速大多数开发人员再也不需要接触一行可执行代码的那一天。
粗略地考虑一下,上面引用的解决方案的执行准确率在 60%到 80%之间,ML 驱动的软件工程师不会很快淘汰人类程序员。
然而与此同时,我们仍然可以享受增强编程来挑战当今软件工程的许多缺陷(文档自动生成、适当的测试编写、代码优化、建议……)
关于代码的机器学习来了!
Ludwig 和 Comet.ml 的无代码深度学习管道
如何在命令行中结合使用 Ludwig 和 Comet.ml 来构建强大的深度学习模型——使用示例文本分类模型
Ludwig 是一个基于 TensorFlow 的工具箱,允许用户在不需要编写代码的情况下训练和测试深度学习模型*。*
通过从头到尾提供一个定义良好的、无代码深度学习管道,Ludwig 使从业者和研究人员能够快速训练和测试他们的模型,并获得强大的基线来比较实验。
“Ludwig 帮助我们在不编写代码的情况下建立最先进的模型,通过将 Ludwig 与 Comet 集成,我们可以以可重复的方式跟踪我们所有的实验,获得可见性,并更好地了解研究过程。”— Piero Molino,优步人工智能实验室的高级 ML / NLP 研究科学家,Ludwig 的创造者
Ludwig 提供了用于预处理数据、训练、发布预测和可视化的 CLI 命令。在本帖中,我们将向您展示如何使用 Ludwig,并使用 Comet.ml 跟踪您的 Ludwig 实验。
参见路德维希 Github 回购 此处
想要一个快速的图像字幕模型或视觉问答模型?在这 4 个简单的步骤中使用 Ludwig来建立、训练和评估深度学习模型。
在这里的 Comet.ml ,我们对 Ludwig 填补机器学习生态系统空白的潜力感到兴奋。Ludwig 最后采用机器学习模型、训练、数据和可视化的抽象表示的想法,并将它们变成一个自始至终无缝、可执行的管道。
这意味着我们终于可以花更少的时间:
- 处理不同数据类型的数据预处理☠️
- 将不同的模型架构网格化只是为了得到简单的基线模型
- 编写代码进行预测
更多时间:
- 获得透明的结果🚀
整合彗星与路德维希
我们与 Ludwig 团队合作整合 Comet.ml 以便用户可以在训练时实时跟踪基于 Ludwig 的实验。
Comet.ml 在三个主要领域对 Ludwig 进行了补充:
- **比较多个路德维希实验:**路德维希让你轻松训练,迭代不同的模型和参数集。Comet 提供了一个接口来帮助你跟踪那些不同实验的结果和细节。
- 为您的分析组织的商店: Ludwig 允许您围绕训练过程和结果生成很酷的可视化效果。Comet 允许您跟踪这些可视化效果,并自动将它们与您的实验关联起来,而不是将它们保存在某个地方。
- **你的实验的元分析:**你可能会多次重复你的路德维希实验。用 Comet 跟踪它们使您能够分析诸如哪些参数起作用之类的事情,以便构建更好的模型。
通过用 Comet.ml 运行您的 Ludwig 实验,您可以捕获您的实验的:
- 代码(您使用的命令行参数)
- 实时性能图表,以便您可以实时查看模型指标(而不是等到培训完成之后)
- 你和路德维希一起创作的可视化作品
- 环境详细信息(例如包版本)
- 运行历史记录(HTML 选项卡)
…以及更多!
用彗星运行路德维希
- 安装 Ludwig for Python(和 spacy for English 作为依赖项,因为我们在这个例子中使用了文本特性)。这个例子已经用 Python 3.6 测试过了。
$ pip install ludwig
$ python -m spacy download en
如果在安装 gmpy
时遇到问题,请安装 libgmp
或 gmp
。在基于 Debian 的 Linux 发行版上: sudo apt-get install libgmp3-dev
。MacOS 上: brew install gmp
。
2.安装 Comet:
$ pip install comet_ml
3.设置您的 Comet 凭据:
- 在 https://www.comet.ml 获取您的 API 密钥
- 让 Ludwig 可以使用 API 键,并设置 Ludwig 实验细节要报告给哪个 Comet 项目:
$ export COMET_API_KEY="..."
$ export COMET_PROJECT_NAME="..."
4.我们建议您为每个 Ludwig 实验创建一个新目录。
$ mkdir experiment1
$ cd experiment1
一些背景: 每次你想创建一个新的模型并训练它的时候,你会用到两个命令中的一个——
—训练
—实验一旦使用
*--comet*
标志运行这些命令,就会创建一个*.comet.config*
文件。这个*.comet.config*
文件从您上面设置的环境变量中提取您的 API 键和 Comet 项目名。
如果您想运行另一个实验,建议您创建一个新目录。
5.**下载数据集。**对于这个例子,我们将使用 Reuters-21578 这个众所周知的新闻专线数据集来处理文本分类用例。它只包含 21,578 个新闻专线文档,分为 6 个类别。两个是“大”类别(许多正面文档),两个是“中”类别,两个是“小”类别(很少正面文档)。
- 小类:heat.csv,housing.csv
- 中等类别:coffee.csv、gold.csv
- 大类:acq.csv,earn.csv
$ curl [http://boston.lti.cs.cmu.edu/classes/95-865-K/HW/HW2/reuters-allcats-6.zip](http://boston.lti.cs.cmu.edu/classes/95-865-K/HW/HW2/reuters-allcats-6.zip?source=post_page---------------------------) -o reuters-allcats-6.zip
$ unzip reuters-allcats-6.zip
6.定义我们希望用我们想要的输入和输出特性构建的模型。用这些内容创建一个名为model_definition.yaml
的文件:
input_features:
-
name: text
type: text
level: word
encoder: parallel_cnnoutput_features:
-
name: class
type: category
7.用新的--comet
国旗训练模特
$ ludwig experiment --comet --data_csv reuters-allcats.csv \
--model_definition_file model_definition.yaml
一旦你运行这个,一个彗星实验将被创建。检查 Comet 实验 URL 的输出,并点击那个 URL。
8.在彗星上,你可以看到:
- 您在图表选项卡上的实时模型指标
- 您运行来训练您的实验的 bash 命令以及代码选项卡中的任何运行参数
- Ludwig 正在使用的超参数(默认)在超参数选项卡中
还有更多!看这个样本实验这里
使用彗星标志运行我们的示例文本分类 Ludwig 实验。你可以在这里和这个彗星实验互动
如果您选择使用 Ludwig 制作任何可视化效果,也可以通过运行以下命令将这些可视化效果上传到 Comet 的图像选项卡:
$ ludwig visualize --comet \
--visualization learning_curves \
--training_statistics \
./results/experiment_run_0/training_statistics.json
现在,您已经准备好一起使用 Ludwig 和 Comet 来构建您的深度学习模型了!在这里报名彗星。
将对立的例子编纂为特征
分离健壮和非健壮特征
Photo by Nahel Abdul Hadi on Unsplash
对立的例子对人工智能从业者来说是一个巨大的麻烦,但在人工智能理论中却是一个巨大的红利,有助于我们理解机器学习模型和算法的内部,并为像甘这样的新兴技术注入活力。
因此,毫不奇怪,我将在这里回顾的这篇新论文在业界引起了轰动。
或者,如果您喜欢使用另一个 PDF 阅读器应用程序阅读,请点击这里的。
计算机视觉算法的问题之一(详见 Hinton 的 Coursera 课程)是间接推理。这里的数据流可以用下图表示:
计算机视觉算法的困难在于从物体到标签没有直接的联系。取而代之的是,拍摄物体的照片,用像素表示,算法学习从像素而不是物体本身获得标签。算法不确定你说的标签是什么意思。如果你给它一张绵羊在山坡上吃草的图像,算法并不知道绵羊是白色物体,还是下面的绿色物体,或者上面的蓝色物体。因此,你需要向算法提供更多的训练数据,以说服它一只羊与天空或草地不同。然而,它可能会学到错误的东西。例如,它可以学习基于 fir 或其他次要特征来识别猫和狗。这就是漏洞所在,通过操纵像素,你可以让算法错误地识别对象。
为了对抗对抗性学习,建议更新优化算法,以在对抗性失真下最小化成本函数而不是成本函数的最大值:
在本文中,除了使用对抗性损失函数之外,还提出了识别鲁棒性特征,即在对抗性失真下不改变相关性符号的特征。
关于这篇论文,有一点要提一下,就是所谓的特性实际上是指倒数第二层的激活。在这种情况下,我们可以使用简单的皮尔逊相关,因为所有的非线性都是在前面的层中处理的。
本文试图做的第一件事是理清稳健和非稳健特征。首先,我们使用对抗性损失函数来训练模型,然后用仅生成稳健特征的修改数据来替换训练数据:
这里的条件是只有鲁棒特征与标签相关,而非鲁棒特征与标签的相关性为零。这个数据集是通过使用反向传播来发现的,但是更新输入,而不是权重和偏差(很像在白盒对抗学习中),最小化倒数第二层的激活的平方差:
在获得修改的数据集之后,对其执行另一轮训练,这一次使用标准(非对抗性)训练。最终的模型被发现在标准和敌对的环境下都能很好地工作。这个结果真的是个好消息,因为使用对抗性损失函数训练非常慢,你也不想太频繁。这里我们只做一次,然后生成新的测试数据,然后使用高效的标准训练程序来做我们的训练实验。在我看来,这是论文最好的实用结果。
以类似的方式,您可以生成非稳健数据集,而无需使用对抗性训练。结果如下图所示:
在我看来,作者没有实现健壮和非健壮特征的分离,因为使用了不同的训练程序,所以两者之间不可避免地存在重叠。
在接下来的实验中,作者故意给这些例子贴上随机标签。他们发现,使用鲁棒特征训练的模型会记住原始数据集,并纠正错误标记的数据本身,而标准分类器会学习新的和不正确的分类。这既是好消息,也是坏消息。虽然它表明该模型对错误标记是鲁棒的,但这也意味着降低了迁移学习的泛化能力和有用性。
本文的理论框架在于将数据点表示为两个高斯分布的混合,对应于二分类问题中的两个类别。使用最大似然法找到分布的参数,并且这些参数对应于逻辑回归中的参数。这是统计学习中的一种已知方法,可以追溯到几十年前。对抗性鲁棒学习使用相同的方法,但是扩展了样本以包括所有可能的对抗性例子:
因此,增加对抗性噪声 ε 有效地增加了习得的σ,混合了组合分布。
结论
本文是对抗鲁棒学习的一般框架的发展,在过去几年中一直在积极工作。它不再关注模型,而是将注意力转移到数据上,根据原始数据生成数据集,经过训练后,这些数据集会生成一个健壮的模型。我认为,这是本文的最大贡献,因为你可以节省时间,避免进行缓慢而昂贵的对抗性训练,而不是第一次为了产生稳健的数据集。
参考
- 安德鲁·易勒雅斯、什巴尼·桑图尔卡、迪米特里斯·齐普拉斯、洛根·恩斯特罗姆、布兰登·特兰、亚历山大·马德瑞。对立的例子不是 bug,它们是特性。https://arxiv.org/abs/1905.02175
用 Python 从头开始编写 2 层神经网络
本系列的第二部分:从头开始编写一个神经网络。用它来预测恶性乳腺癌肿瘤
在本文的第 1 部分 中,我们了解了我们的 2 层神经网络的架构。**现在是建造它的时候了!**同时,我们将深入探索和理解深度学习、反向传播和梯度下降优化算法的基础。
Navigating the Loss Landscape within deep learning training processes. Variations include: Std SGD, LR annealing, large LR or SGD+momentum. Loss values modified & scaled to facilitate visual contrast. Visuals by Javier Ideami@ideami.com
重要的事情先来
首先,如果你想尝试你自己的编码以及这篇文章,一个选择是使用 Jupyter 笔记本。它们极大地促进了在一个对数据探索者和研究人员非常友好的环境中使用 Python 代码进行工作和实验。你可以免费使用 Jupyter 笔记本,比如在谷歌实验室**:colab.research.google.com。**
所以,我们开始吧!首先我们导入一些标准的 Python 库。 Numpy 将帮助我们实现线性代数和数组功能。当我们导入和准备数据时,数据框将会非常方便。Matplotlib 将帮助我们做一些很酷的图表。最后, sklearn 帮助我们标准化我们的数据并显示有用的图表,例如混淆矩阵。
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn import preprocessing
from sklearn.preprocessing import MinMaxScaler
from sklearn import metrics
from sklearn.metrics import confusion_matrix
import itertools
接下来,我们创建一个 Python 类来设置和初始化我们的网络。
class dlnet:
def __init__(self, x, y):
self.X=x
self.Y=y
self.Yh=np.zeros((1,self.Y.shape[1])) self.L=2
self.dims = [9, 15, 1] self.param = {}
self.ch = {}
self.grad = {} self.loss = []
self.lr=0.003
self.sam = self.Y.shape[1]
我们首先命名我们的类 (dlnet),并定义它的 init 方法。第一次实例化该类时, init 方法被执行。它负责创建网络结构和控制网络的方法。
然后我们创建一系列保存网络关键数据的类变量。
这些变量中有很多都是矩阵。请记住,我们将通过使用矩阵合并多个计算来加速我们的计算。正如我之前指出的,如果你想刷新你的线性代数,可以查看 YouTube 上令人惊叹的视频 3Blue1Brown ,特别是他的线性代数系列精华。
- X :保存我们的输入层,我们给网络的数据。我们用变量 **x,**的值来初始化它,当我们创建它时,这个变量被传递给网络。 X 是一个矩阵,它的行数与特征拥有的数据一样多(稍后将详细介绍),列数与我们可用来训练网络的样本一样多。
- Y :保存我们想要的输出,我们将用它来训练网络。我们用变量 y 的值初始化它,当我们初始化它时,它被传递给网络。
- Yh :保存我们的网络产生的输出。它应该与我们期望的目标值 Y 具有相同的尺寸。我们将其初始化为零。
- L :保存我们网络的层数,2。
- 接下来,我们定义每一层中神经元或单元的数量。我们用 numpy 数组来做这个。数组的第一个组件是我们的输入(它不被算作网络的一层)。我们的输入将有 9 个单元,因为正如我们稍后将看到的,我们的数据集将有 9 个有用的特征。接下来,神经网络的第一层将有 15 个神经元,我们的第二层也是最后一层将有 1 个(网络的输出)。
- param :一个 Python 字典,将保存网络各层的 W 和 b 参数。
- ch :一个缓存变量,一个 python 字典,它将保存一些我们在梯度下降算法的反向传递中需要的中间计算。
最后,我们声明另外三个参数。
- lr :我们的学习率。这设定了网络学习的速度。
- 山姆:我们拥有的训练样本的数量。
- loss :我们将存储网络每 x 次迭代的损耗值的数组。损失值表示我们的网络的预测输出和目标输出之间的差异。
注意最后一个元素,损失,因为它是至关重要的。损失值是多少?这一切都与训练我们的网络学习神秘功能的过程有关。
我们很快就会谈到这一点,但首先,让我们定义函数 nInit ,它将用随机值初始化我们网络的参数。
def nInit(self):
np.random.seed(1)
self.param['W1'] = np.random.randn(self.dims[1], self.dims[0]) / np.sqrt(self.dims[0])
self.param['b1'] = np.zeros((self.dims[1], 1))
self.param['W2'] = np.random.randn(self.dims[2], self.dims[1]) / np.sqrt(self.dims[1])
self.param['b2'] = np.zeros((self.dims[2], 1))
return
当我们将矩阵相乘时,就像乘积 W1 X 和 W2 A1 一样,为了使乘积成为可能,这些矩阵的维数必须正确。这就是为什么我们必须正确设置权重和偏好矩阵的维度。
- W1 :行数是该层的隐藏单元数,dims[1],列数是上一层的特征数/行数(本例中为 X,我们的输入数据),dims[0]。
b1 :行数与 W1 相同,单列。
W2 :行数是该层的隐藏单元数,dims[2],列数也是该层的输入行数,dims[1]。
b2: 行数与 W2 相同,单列。
现在,让我们在类中定义一个函数,它将在我们网络中每一层的每个单元执行计算。我们称之为 forward ,因为它将接受网络的输入,并通过不同的层向前传递,直到产生输出。
我们还需要声明 Relu 和 Sigmoid 函数,它们将计算每层输出的非线性激活函数。
def Sigmoid(Z):
return 1/(1+np.exp(-Z))def Relu(Z):
return np.maximum(0,Z)def forward(self):
Z1 = self.param['W1'].dot(self.X) + self.param['b1']
A1 = Relu(Z1)
self.ch['Z1'],self.ch['A1']=Z1,A1
Z2 = self.param['W2'].dot(A1) + self.param['b2']
A2 = Sigmoid(Z2)
self.ch['Z2'],self.ch['A2']=Z2,A2 self.Yh=A2
loss=self.nloss(A2)
return self.Yh, loss
Relu 和 Sigmoid 函数声明激活计算。forward 函数执行我们之前描述的计算。
我们将第一层的权重乘以输入数据,并添加第一个偏置矩阵 b1 ,以产生 Z1 。然后我们将 Relu 函数应用于 Z1 以产生 A1 。
接下来,我们将第二层的权重矩阵乘以其输入 A1 (第一层的输出,即第二层的输入),并添加第二个偏置矩阵 b2 ,以产生 Z2 。然后,我们将 Sigmoid 函数应用于 Z2 以产生 A2 ,它实际上是网络的输出 Yh 。
就是这样!**我们只是通过网络运行我们的输入数据,并产生 Yh,**一个输出。
合乎逻辑的下一步是找出我们的结果有多好。
为此,我们可以将我们产生的输出与我们应该获得的输出进行比较: Yh 和 Y 。为了进行计算,我们将向网络添加最后一个函数损失函数。
希望没什么损失
损失函数有很多种。损失函数的目标是表示我们的结果离预期目标有多远,并对我们用来训练网络的所有样本的差值进行平均。
深度学习中使用的最简单的损失函数之一是 MSE、或均方误差。
**squared_errors** = (self.Yh - self.Y) ** 2
**self.Loss=** np.sum(squared_errors)
MSE loss 函数计算差异,即我们使用的所有样本的预测输出和目标输出之间的距离,然后计算该差异的平方。
最后,它将所有这些操作相加。距离的平方确保我们产生一个总是正的绝对距离值。
**MSE 是一种简单的方法,用于确定我们离目标有多远,**我们的网络计算出的函数在连接输入数据和目标输出方面有多精确。
MSE 常用于回归挑战,当网络输出为连续值时,例如:温度值或房屋成本。
然而,在本文中,我们将致力于一种不同的挑战,一种二元分类挑战,其中我们的输出将是 0 或 1 (0 表示良性,1 表示恶性)。
在处理分类挑战时,有一个不同的损失函数可以更好地表达我们的预测输出和正确输出之间的差异。它被称为交叉熵损失函数,我们将在我们的网络中使用它。
我们选择损失函数的依据是它们在多大程度上表达了我们的网络性能质量与我们正在应对的特定挑战的关系。交叉熵对于分类问题来说是一个巨大的损失函数(就像我们将要研究的那个一样),因为它强烈地惩罚了那些有把握但却是错误的预测(就像以很高的把握预测一个肿瘤是恶性的,而实际上它是良性的)。
def nloss(self,Yh):
loss = (1./self.sam) * (-np.dot(self.Y,np.log(Yh).T) - np.dot(1-self.Y, np.log(1-Yh).T))
return loss
正如你在上面看到的,在 forward 函数的末尾,我们调用这个 nloss 方法(它计算损失),然后将得到的损失值存储在 loss 数组中。这将使我们稍后能够绘制并直观地理解损失值在网络训练期间如何变化。
信不信由你,我们已经创建了几乎一半我们需要的代码。但是现在我们到了关键时刻。
我们已经计算了一个产量 Yh ,并且**计算了损失:**我们离预期产量有多远, Y 。现在的问题是:怎样才能提高那个结果 Yh,提高是什么意思?
为了改善结果,我们需要得到那个损失值来减少。我们的损失值越低,我们的目标和预测输出( Y 和 Yh )之间的距离就越小,我们的网络性能就越好。
让我们回顾一下。我们在网络中产生两种输出:
- Yh ,网络的计算结果。
- 损耗,Yh 和 Y 之间的距离**。**
并且正是从那个目标,从最小化损失的目标,最小化我们的预测和正确输出之间的距离的目标,网络的训练过程诞生了。
我需要一个健身房
在这个阶段,我们已经执行了一次正向传递,获得了我们的输出 Yh ,然后计算我们的损失、我们的误差、我们的预测输出和正确输出之间的距离( Yh 和 Y )。
下一个合乎逻辑的步骤是**稍微改变我们网络的参数值、**我们的权重和偏差、并再次执行前向传递,以查看我们的损失是否有望减少。因此,培训过程应该是这样的:
- 我们最初将权重和偏差设置为随机值。
- 我们通过网络向前运行输入数据,并产生一个结果:Yh。
- 我们计算预测输出和目标输出之间的损耗,即 Yh 和 Y 之间的距离。
- 如果不够好,我们稍微改变我们的权重和偏差,并再次通过网络运行输入数据,看看我们的损失是否有所改善,如果现在足够低。如果没有,我们不断重复同样的过程(大概几千年)。
你说对了,那一点效率都没有。
现在,想一想,要稍微改变我们的权重和偏差,我们可以做两件事情中的一件:
- 我们可以增加一点
- 我们可以减少一点。
一定有办法,以我们的损失为起点,我们可以计算我们是否应该增加或减少 以使这样的损失最小化。
输入微积分,输入强大的导数,梯度。让我们用非常简单的方法来探索导数是如何工作的。如果你想更深入,我会用 3Blue1Brown E 微积分的本质系列再给你介绍一遍。
Hello 渐变
为了了解我们应该朝哪个方向改变我们的权重和偏好,最好是了解这些权重和偏好的微小变化会对我们的最终亏损产生什么影响。
而我们可以对这个使用导数,准确的说是偏导数。因为偏导数将告诉我们一个特定参数的微小变化,比如说 W1 ,对我们的最终损失有什么影响。
有了这些信息,我们将能够决定在什么方向上修改 W1 以减少损失。
先来刷新一下导数的直觉**。**
想想函数 x 的 2 次幂:x2**
在 x=3,y=9 时。让我们把注意力集中在那个点上,求导数,x=3 时的变化率。
为了做到这一点,我们将研究当我们把 x 稍微增加一点时,y 会发生什么。这个微小的量最终收敛到 0(极限),但出于我们的目的,我们将认为它是一个非常小的值,比如 0.001。
**那么,当 x=3+0.001 ,*y 的值是多少呢?y =(3.001) * 2 =9.006
因此当 x 增加 0.001 时,x=3.001 变成 y=9.006。
****而变化率(导数)就是新的 f(x+h)和之前的 f(x)之差,除以那个微小的增量 h:(9.006–9)/0.001 =6。
6 告诉我们什么?
6 告诉我们,在这个函数 x2 中,在 x=3 时,变化率为正,强度为 6。它告诉我们,在这一点上,如果我们增加 x 一点,y 会以一种积极的方式改变,并且强度是“6 倍以上”。基本上是**,输入端的 0.001 增量将变成输出端的 0.006 增量。****
所以我们看到,我们可以很容易地按照这种方法手动计算导数:
导数= (f(x+h) — f(x) ) /h
那么在 x=-2** 时呢?
dx = f(-2+0.001)-f(-2))/0.001
dx = f(-1.999)-f(-2))/0.001
dx = 3.996–4/0.001 =-4**
在 x=-2 处,变化率为负,函数向下移动,强度为 4。
用这种方法计算导数需要很长时间**,但是感谢数学天才,通过使用微分方程,表达原始函数导数的特殊方程,很容易快速计算它们。因此,不用计算每一点的导数,一个简单的方程就能自动计算出这个函数的所有地方!**
大多数,但不是所有的方程,都有一个可以用另一个方程表示的导数。
**x2 的导数 i s 函数 2x 。我们用字母 d 来表示导数,后面是我们正在研究的变量的变化率。
如果 dx =2x:
- 当 x 为 3 时,dx 等于 2*3 = 6
- 当 x 为-2 时,dx 等于 2-2 = -4*
- 两者都符合我们的手工计算。使用微分方程要快得多!
好吧,让我们回顾一下。借助导数,我们可以了解当我们修改某个输入变量(本例中为 x)时,函数的输出在某个点朝哪个方向变化。
如果我们能够使用导数来了解我们的权重和偏差的微小变化如何影响网络损耗,那就太好了。
- 如果我们看到损失相对于重量的导数为正**,这意味着增加重量会使损失增加。也就是说:反其道而行之,减少体重使损失减少。**
- 而如果我们发现导数是负的**,就意味着增加重量使得损失减少。这就是我们想要的!。所以我们接着进行增加权重的值。**
- 因此,通过观察损耗相对于网络参数的导数,我们可以了解改变该参数对网络损耗的影响。
- 基于此,我们可以修改该参数,使其影响朝着降低损耗的方向移动。
不过,还有一个小问题,最后一个障碍。
我们的网络是由层构成的。它可以有 2 层或 200 层。我们需要了解所有权重和偏差的变化如何影响网络末端的损耗。
记住,我们的网络是一系列连锁在一起的功能。例如,如果我们想在多层网络中计算 W1 的变化如何影响最终输出端的损耗,,我们需要找到一种方法来连接 W1 和网络末端损耗之间的不同导数**,使它们相互关联。**
我们能不能,以某种方式,锁住他们?
链式法则
是的,确实如此。微积分给了我们一个叫做的东西,导数的链式法则**,当我们仔细看的时候,这确实是一个非常简单的概念。**
首先,偏导数是研究当我们修改一个变量时,另一个变量发生的变化的导数。
要将这一切与即将到来的代码联系起来:
- 例如,我将把损失**相对于输出 Yh 的偏导数命名为 dLoss_Yh**
- 即:损失函数相对于变量 Yh 的导数。
- 也就是说:当我们稍微修改 Yh,对损失有什么影响?
****链式法则告诉我们,为了理解一个变量的变化对另一个的影响,当它们彼此远离时,我们可以通过乘以它们的将它们之间的偏导数链接起来。
是时候谈谈神经网络中的反向传播算法了,在这种情况下,特别是在我们的 2 层网络中。
****反向传播利用链式法则找出网络不同参数的变化对其最终损耗值的影响程度。
让我们挑选一个参数,了解链式法则的作用。
假设我们想了解 W1 的微小变化会如何影响损失。好吧,让我们从损失的等式开始:
损失= -(Y Log Yh + (1-Y) Log (1-Yh))
嗯, W1 不在这个等式里,但是 Yh 在。让我们继续计算我们的结果 Yh 的变化如何影响损失。让我们看看,在我们这样做之后,我们是否可以继续链接导数,直到我们到达 W1 。
为了计算这个导数,我们寻找损失函数的导数方程。稍微刷新一下微积分,或者上网查一下,就能学会快速找到各类方程的导数。在这种情况下,我们发现:
dLoss _ Yh =—(Y/Yh—(1-Y)/(1-Yh))
好了,搞定一个。现在,请记住,我们希望继续链接导数,直到我们到达 W1** 。**
让我们看看,我们网络的下一步是什么,我们是如何生产 Yh 的?
Yh =乙状结肠(Z2)
好吧,很好。W1 仍然不存在,但是我们得到了 Z2。所以我们来看看 Z2 的变化对 Yh 有什么影响。为此,我们需要知道 sigmoid 函数的导数,恰好是:
dSigmoid = sigmoid(x)(1.0—sigmoid(x))。*
为了简化书写,我们将该微分方程表示为 dSigmoid 。因此:
dYh _ Z2=dSigmoid(Z2)
在这一阶段,我们已经可以将这两个导数链接起来(相乘),以找到损失相对于 Z2 的导数。
dLoss _ Z2=dLoss _ Yh * dSigmoid(Z2)
太好了,我们继续吧。我们是怎么算出 z2 的?
Z2 = W2 A1 + b2
还是那句话, W1 还是没有,但是我们得到了 A1 。让我们看看 A1 的变化对 Z2 有什么影响。因此:
dZ2_A1 = W2
我们可以将该导数链接到之前的 2 个,以获得 A1 和网络损耗之间的总导数:
dLoss _ A1=W2***dLoss _ Z2**
如你所见,我们正在一个接一个地链接导数,直到我们到达 W1,我们的目标。到目前为止,我们已经从损失转移到 Yh,从 Yh 转移到 Z2,从 Z2 转移到 A1。
非常好。我们继续。我们是如何产生 A1 的?
A1 = Relu (Z1)。
W1 仍然不存在,但是我们有了 Z1。我们需要 Relu 的导数。当输入为 0 或小于 0 时,Relu 函数的导数为 0,否则为 1。
同样,为了简化写法,我们将它表示为 dRelu 。
dA1_Z1=dRelu (Z1)
很好,让我们再次将这个最新的导数与所有之前的导数链接起来,以获得损失相对于 Z1 的完整导数:
dLoss _ Z1=dLoss _ A1 * dRelu(Z1)
太好了,我们正在接近!我们是如何计算 Z1 的?
Z1 = W1 X+ b1
是啊!W1 在那里!我们很想你 W1!见到你真是太好了!😃
太刺激了!因此这将是最终的导数:
dZ1_W1 = X
让我们把这个最新的衍生产品和之前的联系起来:
dLoss_W1= X * dLoss_Z1
仅此而已。我们已经计算了损耗相对于参数 W1 的导数。也就是我们稍微修改 W1 的时候,损耗变化多少,往哪个方向变化。
让我们回顾一下:
- 我们已经从网络的末端开始,在损失值处,逐渐链式衍生,直到到达 W1。
- 通过链式法则**,我们开始一个接一个地将所有这些导数相乘,以找到最终的变化率**,即 W1 的变化对网络输出损耗的影响。****
在 Python 代码中,正确排序以说明我们乘矩阵的方式,这个链接过程的代码是:
dLoss _ Yh=——(NP . divide(self。y,self。Yh ) — np.divide(1 —自身。y,1 —自我。Yh))
dLoss _ Z2=dLoss _ Yh dSigmoid(self . ch[’ Z2 '])
dLoss _ A1= NP . dot(self . param[" W2 "])。t,dLoss _ Z2)
dLoss _ Z1=dLoss _ A1 dRelu(self . ch[’ Z1 '])
dLoss _ W1= 1。/自我。x . shape[1]* NP . dot(dLoss _ Z1,self。X.T)**
请注意,在最后一步,我们将结果除以层的单元数,,从而使与每个权重 W 相关的导数在每个单元上正确缩放。
你刚刚看到的是反向传播**,或者说**几乎是所有深度学习过程的关键成分。****
让我们呼吸吧!这是整篇文章中最难的部分,从现在开始事情变得容易了。
- 通过计算 W1 的变化对输出端损耗的影响,我们现在可以决定如何修改 W1 以降低该损耗。
- 如果导数为正,这意味着 W1 的变化增加了损失,因此:我们将减少 W1。
- 如果导数是负的,这意味着对 W1 的改变减少了损失,这就是我们想要的,所以:我们将增加 W1 的值。
- 我们对 W1 所做的,我们将以完全相同的方式对 W2、b1 和 b2 做。
通过这种方式,我们产生了反向传递,它成为了我们的 python 类的反向传播函数。我们还声明了 dRelu 和 dSigmoid,它们是 Relu 和 Sigmoid 函数的派生物,在计算反向传播算法时需要用到它们。
def dRelu(x):
x[x<=0] = 0
x[x>0] = 1
return xdef dSigmoid(Z):
s = 1/(1+np.exp(-Z))
dZ = s * (1-s)
return dZdef backward(self):
dLoss_Yh = - (np.divide(self.Y, self.Yh ) - np.divide(1 - self.Y, 1 - self.Yh))
dLoss_Z2 = dLoss_Yh * dSigmoid(self.ch['Z2'])
dLoss_A1 = np.dot(self.param["W2"].T,dLoss_Z2)
dLoss_W2 = 1./self.ch['A1'].shape[1] * np.dot(dLoss_Z2,self.ch['A1'].T)
dLoss_b2 = 1./self.ch['A1'].shape[1] * np.dot(dLoss_Z2, np.ones([dLoss_Z2.shape[1],1]))
dLoss_Z1 = dLoss_A1 * dRelu(self.ch['Z1'])
dLoss_A0 = np.dot(self.param["W1"].T,dLoss_Z1)
dLoss_W1 = 1./self.X.shape[1] * np.dot(dLoss_Z1,self.X.T)
dLoss_b1 = 1./self.X.shape[1] * np.dot(dLoss_Z1, np.ones([dLoss_Z1.shape[1],1]))
self.param["W1"] = self.param["W1"] - self.lr * dLoss_W1
self.param["b1"] = self.param["b1"] - self.lr * dLoss_b1
self.param["W2"] = self.param["W2"] - self.lr * dLoss_W2
self.param["b2"] = self.param["b2"] - self.lr * dLoss_b2
在后向函数中,在计算完 W1、b1、W2 和 b2 所需的所有导数后,我们在最后一行中通过减去导数并乘以我们的学习速率来更新我们的权重和偏差。
请记住,学习速率是一个允许我们设置网络学习速度的参数。因此,我们通过与学习速度成比例的数量来修改我们的权重和偏差。
好的,目前为止我们有:
- 向前传球
- 计算网络的损耗
- 执行了反向传递并更新了我们网络的参数(以便在下一次正向传递中损耗会降低)。
事实上,梯度下降优化算法,这是训练我们神经网络的另一个迷人的难题。
是时候回到本文的第一个动画了。
沿着梯度下降
我们再来看看文章的第一个动画。
Navigating the Loss Landscape. Values have been modified and scaled up to facilitate visual contrast.
这就是梯度下降优化算法**,这是逐步优化我们网络的权重的基础和最常用的方法,最终它们将允许我们计算一个函数,该函数将我们的输入数据与我们期望的输出准确有效地联系起来。**
让我们分析一下动画中发生了什么,它代表了梯度下降算法的关键方面。****
- 我们首先用随机值初始化我们的权重和偏差。我们建立我们训练过程的初始状态**。**
- 然后,我们通过网络馈送输入数据,以执行正向传递,并获得 Yh 。有了 Yh 我们现在就可以计算损失:我们离理想结果 Y 还有多远?我们的目标是减少损失,使它尽可能小。
- 你在动画中看到的是我们网络的可能损失值的一幅风景。
- 地貌有山丘和山谷。丘陵是损耗高的地方。山谷是我们称之为“极小值”的地方,在那里损失很低。
- 一个函数可以有多个局部最小值和一个全局最小值**。全局最小值是景观的最低部分,是最低的可能损失值。可能有其他的谷是局部极小值,这些地方的损失很低,但还没有低到可能的程度。**
- 当我们最初用随机值设置我们的权重和偏差,并执行我们的第一次向前传球来计算我们的第一次损失值时,就好像我们正在将球随机定位在该动画中的初始景观部分。
- 我们在可能损失值范围内的某个地方随机抛球。
- 通常,我们会将它放在该地形的一个山丘上,因为开始时权重是随机的,网络不是很有效,损耗会很高,我们会被定位在该地形的一个高地(高损耗)
- 我们的目标是从初始点开始,逐渐向高处的一个山谷移动,希望到达全局最小值(最低的山谷),即损失尽可能小的一部分。
- 为了做到这一点,我们执行梯度下降算法**。我们已经在前一节中看到了它的一个迭代。我们现在要做的就是让**不断重复同样的过程,直到我们的损失变得足够小。****
- 我们执行向前传递,计算损失,然后执行向后传递来更新我们的权重和偏差。
- 然后,我们重复相同的过程多次(预先设置),或者直到损失变得稳定。
****
让我们看一下代码:
nn = dlnet(x,y)
nn.gd(x, y, iter = 15000)def gd(self,X, Y, iter = 3000):
np.random.seed(1)
self.nInit()
for i in range(0, iter):
Yh, loss=self.forward()
self.backward()
if i % 500 == 0:
print ("Cost after iteration %i: %f" %(i, loss))
self.loss.append(loss)
return
就这样?对,就是这样。
我们首先实例化我们的神经网络。然后运行多次迭代,执行向前和向后传递并更新我们的权重。每 x 次迭代,我们打印损失值。
经过不到 100 行的 Python 代码,我们有了一个全功能的 2 层神经网络,它执行反向传播和梯度下降。
这是一个基本的网络,现在可以在许多方面进行优化。因为正如我们将很快讨论的,神经网络的性能受到许多关键问题的强烈影响。两个非常重要的是:
- 特征工程(Feature engineering):理解我们的输入数据,并以一种使网络工作更容易的方式准备它。
- ****超参数优化:神经网络的超参数是那些对训练过程有关键影响的变量,不包括权重和偏差。它们包括:学习率,层数,每层单元数等。
关于优化算法,在本文中,我们使用最简单和最纯粹的梯度下降算法。我们的目的是了解反向传播和基本的优化和训练过程。
梯度下降有很多种变体,稍后我会列举其中几种。两个非常简单的例子是:
- 批量梯度下降:我们不是在整个训练集上运行我们的正向和反向通道,而是批量运行它们。假设我们有 1000 行数据。我们将 1000 行分成 10 批,每批 100 行。然后,我们将对第一批(前 100 行)运行训练循环(向前和向后传递),并继续更新我们的权重。然后,我们继续进行其余的批次。当我们完成所有批次(本例中为 10 个)时,这被称为一个时期。然后我们继续下一个纪元。处理大型数据集时,批量梯度下降训练比纯梯度下降算法更快,因为您更新权重的频率更高(您不必等到处理完整个数据集后再更新网络参数)。
- 随机梯度下降:当你的批次的大小为 1 时,这叫做随机梯度下降。它甚至更快,因为您在处理每一个样品后都会更新您的重量。但是,由于网络是基于非常少的数据做出决策的,因此它可能会产生不规则的路径(噪声)。有不同的方法来处理这个问题,其中一些将在本文后面提到。
现在是时候测试和尝试我们的网络了。只有使用它,我们才能充分了解它的潜力和局限性。
**在本文的最后部分,第 3 部分 **,我们将使用威斯康星癌症数据集,学习准备我们的数据,通过 out 网络运行它并分析结果。我们还将讨论一些更高级的话题。我们来看 第三部 。
链接到本文的 3 个部分:
第 1 部分 | 第 2 部分|第 3 部分
编码意识
一篇很长的关于图灵测试的帖子
美国哲学家约翰·r·塞尔(John R. Searle)在他举世闻名的 1980 年“思想、大脑和程序”中,以优雅而明确的方式证明了算法的执行(无论它有多复杂)并不是理解的充分条件。在他的格丹肯实验、中文房间论证(CRA)中,他想象着把自己锁在一个房间里,里面有一大堆中文作品(对他来说只是一堆毫无意义的的潦草字迹)和一本用英语写的规则的书,这些规则把这些符号和传给他的其他符号联系起来。从房间外面看到这一过程,没有人会认为他只是在进行符号操作,无论如何,他现在比他第一次走进房间时更懂中文了。这个思想实验证明,正如他在他的工作中几次重新措辞,并在这里以一种简单而有效的形式提出,“符号操纵本身不足以理解”(塞尔,1980),或者更简单,“句法本身不足以构成语义”。
句法本身不足以构成语义。
很明显,许多人对这个实验提出了反对意见,塞尔对几乎所有人都给出了精确的回答;然而,对其中一个最相关的,所谓的系统回复的回答并不令人满意,缺乏连贯性,为一些有趣的结果和进一步的争论敞开了大门。
系统回复声称,即使接受仅仅执行算法不会导致理解,执行者(在 CRA 中,被锁在房间里的人,或者更一般地,图灵机™通过计算机程序的步骤)也只是整个系统的一部分,并且这个系统理解中文(继续使用塞尔的例子)。塞尔对这一回答的回应遵循两个平行的方向:在第一个方向中,他假设将整个系统“内化”到个人内部(达到一种退化的配置,在这种配置中,人实际上是整个系统),并声称即使在这种情况下,系统仍然没有理解;在第二本书里,他认为,把对一个人的理解与一张纸联系起来是多么可笑,这张纸曾经正确地强调了只有这个人不会有任何理解。作为第二个,更多的是对把理解归因于整个系统的假设性后果的分析,重点必须放在第一个上,以便理解它在哪里以及为什么缺乏合理性,鉴于塞尔自己正确地连接到他的论点的后果,更是如此。
在没有深入进入经典哲学的“他者”问题(甚至开始对这些论点进行推理所需的前提)理解是,作为意识、意向性等。“只能从许多不同的角度来理解其客观性质的主观特征。塞尔在他的论证中只谈论理解而不谈论他人(首先是意识),这一事实并不是对后续步骤的限制;在某种意义上,因为哈纳德在 1991 年写道“只有一个身心问题”,在另一个意义上,因为在后来的著作中讨论和进一步阐述这个概念的方式显然可以与意识的概念互换使用。
在试图对这些现象进行一般和连贯的描述时,几十年来提出了不同的理论;这篇文章的目的不是精确地描述所有这些理论,而是为了使它们尽可能地完备,并在接下来的段落中避免概念模糊,将宇宙分成两部分是有用的:第一组理论由那些在计算主义的广义概念下认识自己的理论组成,对于它们来说,心理过程完全是计算性的,并可以通过纯粹的符号操作来描述。这些理论的推论在很大程度上是有争议的,但毫无疑问的是,接受计算主义就是接受至少一些二元论的影子,因此心灵和身体是不同的,可以分割的(强人工智能比其他人更重视这个概念)。后者,通常被称为物理主义,与人类精神现象可能依赖于实际人类大脑的物理/化学特性的想法有关。这第二种类型的理论,把有意现象的诞生不是在计算的行为,而是在“具有某种生物结构的某种有机体”的事实,可以生存到 can 特别是生物自然主义正是塞尔在其论文的最后几段和整个职业生涯中所支持的理论。即使在某种意义上,这种特定的理论将自己置于上述两种划分的中间(考虑到它关于不可还原的精神现象的存在的立场,不被纯物理主义者所接受),它肯定会将产生意向性的能力赋予实际的人脑及其特定的本性。物理主义理论,除了塞尔列出的特性,还有一个很大的优势,那就是不受抽象限制的影响,这些限制在 Gödel、卢卡斯和彭罗斯的作品中有很强的描述。这种限制最初是针对形式系统的,这些系统具有足够的复杂性来表达自然数的基本算术,并且是一致的,随后被利用来对图灵机(或等同物)的能力进行一些限制,以充分模拟人脑的能力,攻击计算主义的基础;“头脑不能被解释为机器”(卢卡斯,1961)。
根据这段解释性的段落,我们可以更好地理解为什么塞尔用来反驳系统回答的内化过程显示出它的不足。论文中没有给出如何进行这一过程的细节,这种缺乏也不能用思想实验的制度来解释,他的所有结论都是在这个制度中得出的。谈论内化打破了假设推理的假设,并且至少需要一个心灵理论作为共同基础,这与所有其他主张都需要一个计算理论的情况没有什么不同。事实上,不能保证内化的过程比传统的学习过程更能导致纯粹的句法执行,传统的学习过程已经导致了对规则所使用的语言的相互同意的实际理解。如果这是真的,正如塞尔自己所支持的,理解不是一个执行的问题,而是大脑的一些化学特性的结果,那么对于一个内化过程,其唯一给定的细节是利用这些特性的事实,又能说什么呢?把这些放在一边是用双重和计算的方式进行推理,使用他自己已经证明是错误的工具——或者需要被大量审查的工具。
事实上,不能保证内化的过程比传统的学习过程更能导致纯粹的句法执行,传统的学习过程已经导致了对规则所使用的语言的相互同意的实际理解。
此外,内化过程(及其分析)打破了他心问题的围墙,就好像存在一个“塞尔的小‘潜望镜’跨越了其他不可逾越的他心障碍”(Hayes et al .al,1992)以图灵不可区分的表现证明了汉语口语理解能力的缺失。这一论点虽然有风险,但增加了整个答复的另一层不确定性,与前一个论点相结合,即使不能完全接受系统的答复,至少也能更令人信服地考虑接受它为真可能产生的后果。
理解在哪里?
简单地说,接受系统的回答就是接受这样一个事实,即个人是系统的一部分,系统理解这个故事。如此简单的声明的结果可能很难管理,并且向两个不同的方向发展:我们将系统的边界放在哪里,以及我们愿意将理解分配给系统的哪些工件。
简单地说,接受系统的回答就是接受这样一个事实,即个人是系统的一部分,系统理解这个故事。
尽管这两个问题都将在稍后进行深入研究(前者是后者的直接结果),但提出一个前提来讨论现在被称为理解的内容是有用的,重点是我们可以在多大程度上研究这个概念。关于理解的本质和价值的争论贯穿整个哲学,但是没有理由不接受塞尔在他论文的注释中给出的定义。他将理解定义为当代拥有的有效且有意的精神状态。定义的核心当然是围绕意向性的概念,即“某些精神状态的特征,通过这些特征,它们指向或关于世界上的事物和状态”。潜在的问题,是所有主观经验所共有的,并且与意识的困难问题相关,在于这些现象的本质,这使得定义有趣,但在某种意义上完全无用。完全接受我们的外部观点的限制,并且在操作我们的探索的当代需要中,我们只能寻找理解的痕迹:通过这些迹象,应用我们在日常社会关系中应用的相同的常识性论点和真实性假设,我们可以假设理解在其他人身上的存在,就像我们把它与我们联系起来一样;或者,类似地,如果没有意向性的归属,我们就很难理解这些符号。
因此,接受上文指出的系统答复的第二个结果是以一种新的形式慢慢重塑,即使在某种意义上可以被视为一种限制,但它将概念建立在一个可以实际处理它的领域中。至于塞尔的担心,虽然一个人不懂中文,但不知何故这个人和纸片的结合可能懂中文,但纸片的想法——尽管这样称呼它是对其内容的有意低估,但这里真正的问题是——是理解的痕迹和证明——不是正式的,不可能的,但在某种最大可能性的情况下——理解是存在的。一个现在不能被忽略的明显问题是,由这种推理引发的想法是,计算中有某种东西具有“诞生”交互式守护进程的能力,这种守护进程能够以行为方式展示理解——与只能显示其被动痕迹的工件明显不同。这个想法在文学上并不新鲜,许多贡献大到足以成为一个平行的理论分支,称为虚拟心灵对 CRA 的回应。与系统的回答类似,VMR 承认接线员一点也不懂中文,但强调重要的事实是理解是否被创造出来,而不是接线员是否真正理解。
重要的事实是理解是否被创造,而不是操作者是否是真正理解的人。
计算行为创造了新的虚拟实体,其心理特征完全取决于程序和中文数据库。尽管这一立场至少需要某种计算主义,也不清楚这如何能与 Gödel 定理共存,但计算因其内在特征而成为一种特殊事物是相关且无可争议的。我们给被锁在房间里的个人的一套规则,不仅是表达表象的媒介,也是引出某些机器的“表象活动”的媒介。“任何计算机程序固有的程序性后果使它在语义学中有了立足之地”,在语义学中,一个符号的意义“是通过参照它与其他现象的因果联系来寻求的”。与符号执行(意思是活动)相关的计算中的这种表示概念将随后根据通过图灵测试所需的非字典基础进行更好的研究。
考虑到这些争论,系统回答的第一个公开问题,即在哪里设置系统的边界,已经失去了一些最初的兴趣:如果我们现在准备好接受任何东西作为理解的痕迹——这并不意味着接受一切,将图灵测试作为一个沉默的看门狗——我们可以在我们想要的任何地方理想地设置边界(如塞尔所指出的,在执行者之外)。无论哪个系统通过了图灵测试,都包含了一段理解,即一组规则(或者,等价地,一个计算机程序)或者编写它的程序员(如果有的话),或者他研究过的书,或者写它的作者…
如果这最后一段看起来不寻常或有点大胆,反而在日常理解和理解的方法中很常见。正如海斯指出的那样:“我读了一本小说,结果陷入了对生命本质的深刻思考,然后决定放弃一切去出家,是小说让我做出了这个决定吗?”从某种意义上来说,这部小说通过了一个非常轻量级的图灵测试,因此,我没有理由认为写它的人不明白他写了什么(可能是遍历同一文本的副本金字塔)。
无论哪个系统通过了图灵测试,都包含了一段理解,即一组规则(或者,等价地,一个计算机程序)或者编写它的程序员(如果有的话),或者他研究过的书,或者写它的作者…
理解不在书中(被认为是印刷的信件、论文等)而是在系统中的想法现在更有说服力了,系统边界的概念看起来更熟悉了。
图灵测试的充分性
塞尔的论文和这篇论文中得出的许多结论都是基于这样一个假设,即图灵测试是一个有效的测试,至少可以推测理解的存在。在他最初的工作中,图灵在避免智力的精确理论定义的方向上和在随后几年的研究的操作方向的需要上提出了这个测试。尽管多年来对智力定义的自愿和有利的逃避引起了一些反对意见,但毫无疑问,利用一种类似于日常生活中使用的行为方法使 TT 变得几乎无懈可击,而无需进入上面指出的同样危险的他人思维话题。因此,图灵不可区分性的概念,不仅是我们在评估智能表现的任务中所要注意的一个起点,而且,以一种循环的方式,也是我们对理解进行推断的唯一实际方法。
区分这两者是很有用的:对智能行为的研究可能非常依赖于领域,当考虑到它的原始版本时,可能会导致测试的一些关键和病理问题;承认这一点,丝毫不会影响我们的研究方向。我们并不是在寻找表现(不管用什么方法来评估它们,也不管在什么方法上可以进行许多其他的讨论),而是在某种意义上,寻找取得这些表现的原因。
我们不是在寻找表现,而是寻找为什么会取得这些表现的原因。
这些概念只能通过它们的性质来操作;攻击图灵测试是对概念的歪曲,是对其适用范围的攻击。图灵测试表明,它不仅足以完成这项任务,而且在我们正在研究的概念被赋予新的意义之前,它也是我们发现它们踪迹的唯一可能性。
“推断思维的唯一可用基础是图灵不可区分的表现能力”。(哈尔纳德,1992 年)
后果和结论
对这些观点的一些可能的批评已经在帖子里提到了,主要涉及到接受这样一种观点,即物理的无生命物体可以表现出理解的痕迹。正如在前面的段落中已经指出的,这种潜力必须总是在图灵测试的框架和在适当的硬件环境中执行所使用的形式主义的能力下被看到。与塞尔部分一致的是,这种归因确实部分地受到“在人工制品中我们扩展我们自己的意向性的事实”的偏见,但是将我们的意向性扩展到足够大的程度以通过图灵测试的能力不是微不足道的,它是整个推理的核心。
将我们的意向性扩展到足够大的程度以通过图灵测试的能力不是微不足道的,它是整个推理的核心。
对这些立场的另一个可能的批评可能与机器学习(ML)领域的最新发现有关。自 20 世纪 50 年代以来,人工智能的研究方向几乎完全致力于专家系统,这些专家系统具有许多领域特定的属性,用于开发高效和有效的启发式算法来解决复杂的搜索问题。近年来,可用计算能力的提高使得人们得以重温一些自 60 年代以来就藏在阁楼上的老方法,这些方法是图灵在他的论文(“儿童计划与教育过程”)中一段巧妙而有远见的话勾勒出来的。简化概念来说,机器学习是涉及使用来自问题本身的数据来自动研究复杂问题的解决方案的领域。这两种范式的关键区别不仅在于表现,还在于用来解决任务的信息来源。如果在启发式的开发过程中,需要人类理解特定问题的能力来允许机器有效地解决它,那么 ML 根本不需要它,它能够从数据中提取相关信息,而无需专门编程来解决考试中的问题。如果前面段落中关于理解及其在系统中的存在的所有主张不会受到能够通过图灵测试的假设专家系统的哲学影响,那么将一些理解附加到能够在没有任何人类干预的情况下完成这一挑战的机器学习系统将是奇怪的。房间里的大象,不仅在这个特定的小生境中,而且在所有关于 ML 的主流讨论中,都在要求使用巨大的标注语料库来喂养这些系统。解决问题的 ML 方法是非常宝贵的,并且每天都变得越来越令人惊讶,但是,从某种意义上说,在这个过程中的人类干预现在比过去使用的方法更重要。如果明天早上一个使用最先进的深度学习技术设计的聊天机器人能够通过图灵测试,那将归功于成千上万标记的人类之间的真实对话,这些对话用相互理解的语言编写,并使用特别的算法学习。理解仍然存在于系统中。
如果明天早上一个使用最先进的深度学习技术设计的聊天机器人能够通过图灵测试,那将归功于成千上万标记的人类之间的真实对话,这些对话用相互理解的语言编写,并使用特别的算法学习。
根据所有的讨论,通过图灵测试的一个必要和充分条件可以被重新表述为管理一种语言并以人类不可区分的方式谈论通过它执行的任务的结果的能力。事实上,人类自己能够通过学习来完成这项任务,这可能意味着机器学习作为一个通用框架,是设计可执行计算机程序的正确框架,可以在未来几年通过图灵测试。这里一个可能的公开问题,但在某种意义上比这篇文章的目标更广泛,就是是否存在一种方法让一个人通过原始版本的模仿游戏——一个男人和一个女人在一个房间里,男人试图在他的性别上欺骗询问者的经典游戏——而不存在一个学习过程,将一些基础附加到符号上,这是老师和学生能够管理的。理解作为教授的能力的存在(或者,回到 ML 的例子,选择合适的例子和学习算法的能力)可能是一个有趣的讨论方向。
编码与世界语言文学:用 Python NLTK 分析但丁的《地狱》
Photo by DR. Alexandru STAVRICĂ on Unsplash
本初级教程向您展示如何使用 Python NLTK(自然语言工具包)分析外语文本,这是一个自然语言处理平台,允许您将单词分解为词干,并确定文本中最常用的单词。
计算机科学可以连接到任何其他学科——所以如果你对编程感兴趣和梦想在意大利别墅的生活,这个教程就是为你准备的!
在本教程中,我们将通过词频来分析但丁的地狱的文本。《地狱》是一首 14 世纪的意大利诗歌,详细描述了作者虚构的地狱之旅。作者但丁由罗马诗人维吉尔带领,他带他参观了地狱的九层。
虽然这首诗已被翻译成英语,但原诗是用意大利语写的。你能预料到分析一首意大利诗歌的词频会有什么问题吗?
考虑一下罗曼语和英语中的动词变化。在英语中,你可以说“我说”和“你说”,而“说”这个词不会改变。主语(做动作的人)用单独的词“我”和“你”来表示但是在意大利语中——很像西班牙语和其他罗曼语——我不得不说parlo(我说)和parlI(你说),通过单词的结尾表示主语。
因此,如果我们分析但丁的地狱中的词的频率,那么 parlo 和 parli 会被算作同一个词吗,来自不定式 parlare ?不会,因为电脑很笨,不懂意大利语。幸运的是,有一个解决方案!
在我们分析文本之前,我们可以使用一个叫做标记化的过程来分离所有的单词。然后我们把每个单词分解成词干。这个过程将把 parlo 和 parli 转换成它们现在的词干 parl- ,允许它们被算作同一个单词。到那时,文本就可以进行分析了。
让我们开始用 Python 编程,这样我们就可以标记但丁的地狱!
Python 入门
总的来说,Python 是一种非常棒的数据分析语言,不管我们更侧重于科学还是人文学科。
要在 web 浏览器中开始使用 Python 编程,请访问 repl.it 并创建一个帐户。之后点击屏幕右下角的红色加号,做一个编码环境:
Click on the red plus sign.
从“流行”类别中选择“Python ”:
然后,您应该会看到一个空白的 Python 编辑器:
You’ll be typing in the white text area.
首先,我们需要从 NLTK 和其他包中导入和下载我们需要的所有东西。将这些导入复制并粘贴到您的 Python 编辑器中,然后阅读下面的解释。
import nltk
from nltk.corpus import stopwords
from nltk.probability import FreqDist
from nltk.stem.snowball import SnowballStemmer
from nltk.tokenize import word_tokenize
from urllib import request nltk.download('punkt')
nltk.download('stopwords')
我们的进口:
- Python 附带的自然语言工具包
- 阻止来自 NLTK 的单词。这些是常见的词,比如“a”和“the ”,我们在分析频率时会想过滤掉它们。许多语言都有停用词,包括意大利语。
- NLTK 的雪球斯特梅尔。这个工具可以让我们将每个单词缩减到它的词干,这样我们就可以准确地计算出它的频率,而不管它的语法上下文。雪球斯特梅尔用外语工作。
- 一个单词标记功能,允许我们将文本分解成单词列表进行处理。
- 一个请求函数,允许 Python 从 URL 中读取文本。
最后,我们从 NLTK 下载了一个名为“punkt”的包,它允许我们使用单词标记化功能,以及另一个名为“stopwords”的包,它允许我们访问上面提到的停用单词。
最后的文体提示:按字母顺序排列你的导入是一个好习惯。
从 URL 导入文本
将但丁的地狱的整个文本复制粘贴到一个单独的文件中真的很难(我试过了),所以我们将使用 Python 从一个网站上读取文本!
我找到了。txt 文件(通常最容易处理),所以我要做的第一件事(在我的导入下面)是将该 URL 存储在一个变量中:
url = "https://www.gutenberg.org/cache/epub/997/pg997.txt"
现在,每当我说url
,电脑就会知道我指的是 gutenberg.org 的地址。
接下来,我可以使用我导入的请求方法向 gutenberg.org 的服务器发送请求,并在服务器响应时获得文本 Inferno :
response = request.urlopen(url)
服务器将交付 Inferno 文本并将其存储在response
变量中,但是它的格式会比我们需要的稍微复杂一些。我们可以用下面的代码行将 Inferno 变成一个易读的文本字符串:
text = response.read().decode('utf8')
基本上,我只是告诉计算机使用 UTF-8(一种常见的字符编码形式)将过于复杂的变量response
翻译成简单的文本字符串。现在变量text
将保存一串简单格式的文本,如下所示:
"Nel mezzo del cammin di nostra vita mi ritrovai per una selva oscura che la diritta via era smarrita..."
基本上,地狱的整篇文字都被引号包围了。在计算机科学中,我们将这种数据类型(由引号包围的一串字符/单词/短语)称为字符串。
好了,我已经得到了友好格式的文本。接下来要做什么?如果你说的是“标记文本”,你答对了!
对文本进行标记
之前,我提到过标记化会产生“要处理的单词列表”列表实际上是 Python 中的一种数据结构(类似于其他语言中的数组)。而变量看起来像这样:
favorite_food = "tiramisu"
列表如下所示:
grocery_list = ["pasta", "tomato sauce", "pesto", "gelato"]
你可以看到,虽然一个变量只包含一个东西,但一个列表包含许多东西,这使得它对于杂货列表和书籍中的单词列表非常有用。当我们标记出 Inferno 时,它会变成这样:
"Nel mezzo del cammin di nostra vita..."
变成这样:
["Nel", "mezzo", "del", "cammin", "di", "nostra", "vita"]
编写令牌化的源代码可能很复杂,但是幸运的是,NLTK 为我们提供了这个功能!要将 Inferno 的原始文本转换成令牌,请编写这行代码:
tokens = word_tokenize(text)
现在,变量tokens
应该包含一个单词列表。如果您想看看这个列表是什么样子(或者至少是它的底部,因为 repl.it 只能打印这么多),您可以像这样打印标记:
print(tokens)
The tokens!
计算机可能需要一点时间来生成令牌。在你等待电脑的时候,沉浸在你的成就中——电脑通常工作得很快,无论何时你让它们变慢,你都知道你正在做复杂、重要的事情(或者不小心写了一个无限循环)。
作为一种捷径,我喜欢只打印令牌列表的长度,也就是说,列表中的单词数。您可以使用len
函数来计算列表的长度:
print(len(tokens))
你应该有 45274 个令牌(或单词)!
如果您打印了令牌,您会注意到还包括了标点符号。什么?!
Some of the tokens are just commas and accents!
幸运的是,我们可以用下面的代码行来摆脱这种疯狂:
tokens = [token for token in tokens if token not in '.,:;<>!?[]()`"\'']
哇哦。这是怎么回事?!基本上,这是一个很酷的编程策略,我说:“将tokens
变量设置为与之前相同的东西(之前存在的所有标记),但过滤掉任何标记,实际上只是我明确列出的任何标点符号!”
如果再次打印令牌的长度,现在应该有 34,836 个令牌。
你可能会注意到,在这些标记中,我们有一些基本的单词,比如“la”(the)和“e”(and)。对于分析词频来说,这些基本单词对我们来说一点也不有趣——谁在乎但丁是否说了 1000 次“the ”?!
这些基本词在 NLTK 里叫做停用词,我们可以过滤掉。
过滤掉停用词
现在我们准备去掉停用词,那些我们不在乎的超级常用词。首先,让我们将停用词存储在一个变量中:
stop_words = stopwords.words("italian")
现在,我们可以做一些有趣的编程工作来从我们的列表中过滤出这些步骤。下面是我们如何将tokens
变量重置为没有停用词的单词列表:
tokens = [token for token in tokens if token not in stop_words]
基本上,我说:“让tokens
变量等于所有和以前一样的记号,除了stop_words
列表中的单词/记号!”
为了再次检查是否所有讨厌的停用词都被删除了,我们可以打印出记号——以及记号的长度,以确保它与以前不同!
print(tokens)
print(len(tokens))
这个列表现在应该有 21781 个单词,比我们之前的 34k 左右的单词少了很多。
我们还可以看到,我们的新令牌列表很高兴地没有填充词和标点符号:
不错!
词干
现在我们准备把我们的话砍进他们的茎!首先,我们需要创建一个词干分析器。您可以像这样创建一个意大利词干分析器:
stemmer = SnowballStemmer("italian")
如果您使用的是不同的语言,请注意,您可以用这行代码打印雪球斯特梅尔处理的所有语言:
print(" ".join(SnowballStemmer.languages))
现在,我们可以用更巧妙的编程方法将所有单词转换成词干:
stems = [stemmer.stem(word) for word in tokens]
上面的代码行基本上解释为,“对于tokens
列表中的每个单词(或标记),将其转换为词干,并将这个新的词干列表存储在一个名为stems
的变量中。”
您也可以用这行代码打印词干:
print(stems)
现在我们已经得到了茎,我们准备进行一些频率分析!
计算词频
现在我们可以尝试基于词频来分析但丁的地狱了!我将这一部分命名为“计算词频”,但计算机将是唯一一台进行数学运算的计算机——我们将只编写几行简洁的代码。这就是 NLTK 的妙处!
要在变量中存储频率分布,我们只需说:
fdist = FreqDist(stems)
在这一点上,一些程序员可能会做数据可视化,但如果我没有弄错的话,repl.it 没有这个功能——如果您想探索数据,您必须将 Python 和适当的包下载到您的计算机上。
但是,我们绝对可以打印出数值!有了这行代码,我可以打印出但丁的地狱中最常见的 100 个单词:
print(fdist.most_common(100))
酷,最常用单词列表!
看词频可以得出什么结论?或者说,检查这些频率引发了哪些问题?
我们可能会注意到,单词 piu (更多),被使用了 181 次,而词干 tant- (更多),被使用了 80 次。这些话表明地狱是一个极端的地方。
我们可能还会注意到词干 l’altr- (另一个)被使用了 94 次,这可能会导致对但丁的地狱中二元性的研究。
我们可以检查最常见的动词,它们与看和说有关,以及出现 50 次的单词 occhi (眼睛)。这些话表明了但丁在冥界的消极态度。
假设你是意大利文学的研究生。你还能想到什么?
Photo by Willian West on Unsplash
进一步阅读
编程是面向所有人的,包括人文主义者!查看下面的参考资料,了解关于 NLTK 和 Python 数据分析的更多信息。
- DataCamp ,学习自然语言处理和其他 Python 数据分析的绝佳资源
- 编程历史学家,一个数字人文教程的伟大资源
- 编码&英语文学:Python 中的自然语言处理,关于分析英语文本的教程
- 如何使用 NLTK Porter &雪球测径器
- 在 Python 中使用 NLTK 删除停用词
R 中的编码:嵌套并映射到高效的代码
Photo by Olivia Colacicco on Unsplash
嵌套并映射到高效代码
我在 2019 年 3 月写了我的第一行 R 代码,但是最近才发现nest
和map
的威力。在此之前,我写了很多效率低下的代码(C & P,C & P,C&P……)。
这是如何使用nest
和map
找到 184 个国家在 1960 年到 2016 年间使用gapminder
数据集(内置于dslabs
)的平均人口的简短演示。gapminder
拥有 184 个国家从 1960 年到 2016 年每年的健康和收入数据。数据以整齐的格式呈现,这意味着每个国家有 57 行数据(每年一行)。
library(dslab
glimpse(gapminder)
`## Observations: 10,545
Variables: 9
$ country Albania, Algeria, Angola, Antigua and Barbuda, …
$ year 1960, 1960, 1960, 1960, 1960, 1960, 1960, 1960,…
$ infant_mortality 115.40, 148.20, 208.00, NA, 59.87, NA, NA, 20.3…
$ life_expectancy 62.87, 47.50, 35.98, 62.97, 65.39, 66.86, 65.66…
$ fertility 6.19, 7.65, 7.32, 4.43, 3.11, 4.55, 4.82, 3.45,…
$ population 1636054, 11124892, 5270844, 54681, 20619075, 18…
$ gdp NA, 13828152297, NA, NA, 108322326649, NA, NA, …
$ continent Europe, Africa, Africa, Americas, Americas, Asi…
$ region Southern Europe, Northern Africa, Middle Africa…`
嵌套并不可怕,你的数据也没有消失
nest
仅在通过group_by
进行操作时有效。在嵌套数据之前,必须对数据进行分组。通过country
对gapminder
进行分组,然后调用nest
,结果如下:
gapminder_nest <- gapminder %>%
group_by(country) %>%
nest()head(gapminder_nest)## # A tibble: 6 x 2
## # Groups: country [185]
## country data
## <fct> <list<df[,8]>>
## 1 Albania [57 × 8]
## 2 Algeria [57 × 8]
## 3 Angola [57 × 8]
## 4 Antigua and Barbuda [57 × 8]
## 5 Argentina [57 × 8]
## 6 Armenia [57 × 8]
我第一次用nest
的时候,就被这个吓坏了。我不是来自编码或 IT 背景,看到<list<df[,8]>>
足以让我删除代码,并恢复到剪切和粘贴来完成工作。
但实际上并不可怕。在这个新创建的名为data
的列中,你看到的是每个国家整齐排列的健康和收入数据。可以通过调用gapminder_nest$data[[x]]
来访问它,其中x
是行。所以要打电话给澳大利亚(我来自的地方),代码是gapminder_nest$data[[8]]
。
gapminder_nest$data[[8]]## # A tibble: 57 x 8
## year infant_mortality life_expectancy fertility population gdp
## <int> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 1960 20.3 70.9 3.45 10292328 9.67e10
## 2 1961 20 71.1 3.55 10494911 9.90e10
## 3 1962 19.5 70.9 3.43 10691220 1.00e11
## 4 1963 19.2 71.0 3.34 10892700 1.07e11
## 5 1964 18.8 70.6 3.15 11114995 1.14e11
## 6 1965 18.6 71.0 2.97 11368011 1.21e11
## 7 1966 18.3 70.8 2.89 11657281 1.24e11
## 8 1967 18.3 71.1 2.85 11975795 1.31e11
## 9 1968 18.2 70.7 2.89 12305530 1.38e11
## 10 1969 18.1 71.1 2.89 12621240 1.48e11
## # … with 47 more rows, and 2 more variables: continent <fct>, region <fct>
绘制你的巢穴地图
map
允许您对数据框中的所有列迭代函数。语法是:map(.x, .f)
,其中.x
是要迭代的数据帧,.f
是函数。嵌套数据的一个小问题是在数据框中有数据框**。在gapminder_nest$data
列中还有**列数据。****
因此,我们需要将gapminder_nest$data
用作.x
,而不是在map
调用中使用gapminder_nest
作为.x
。这意味着map
将迭代每个国家的嵌套数据框。
让我们使用嵌套数据和map
找出 1960 年至 2016 年间每个国家的平均人口。将.x
设置为gapminder_nest$data
。这个功能不是简单的mean
。如果你只是写代码,你的代码会抛出一个错误。记住,列中还有更多列**data
。我们想要平均人口,所以.f
的代码是~mean(.x$population)
。简单地说,这段代码表示迭代所有嵌套的国家数据集,计算每个国家人口列的平均值。不要忘记.f
代码中的波浪号~
!**
(在下面的代码中,我设置了na.rm = T
以避免数据缺失的国家/年份出现任何错误)
pop_mean <- map(.x = gapminder_nest$data, .f = ~mean(.x$population, na.rm = T))
head(pop_mean)## [[1]]
## [1] 2708629
##
## [[2]]
## [1] 24231378
##
## [[3]]
## [1] 11909433
##
## [[4]]
## [1] 71053.36
##
## [[5]]
## [1] 31638376
##
## [[6]]
## [1] 2925011
Mutate 是 nest 和 map 的秘方
然而,这个输出是非常无用的,因为我们不知道国家名称是什么。一个更简洁的解决方案是mutate
现有的gapminder_nest
并添加一个名为pop_mean
的列。
gapminder_nest <- gapminder_nest %>%
mutate(pop_mean = map(.x = data, .f = ~mean(.x$population, na.rm = T)))head(gapminder_nest)## # A tibble: 6 x 3
## # Groups: country [185]
## country data pop_mean
## <fct> <list<df[,8]>> <list>
## 1 Albania [57 × 8] <dbl [1]>
## 2 Algeria [57 × 8] <dbl [1]>
## 3 Angola [57 × 8] <dbl [1]>
## 4 Antigua and Barbuda [57 × 8] <dbl [1]>
## 5 Argentina [57 × 8] <dbl [1]>
## 6 Armenia [57 × 8] <dbl [1]>
但这还是不对。我们可以看到国名,但看不到平均人口。这是因为map
总是返回一个列表,所以平均人口在gapminder_nest
的嵌套列表中。
渴望成功
要查看平均人口,只需在您希望可见的列上调用unnest
,在本例中为pop_mean
。
gapminder_nest %>%
unnest(pop_mean)## # A tibble: 185 x 3
## # Groups: country [185]
## country data pop_mean
## <fct> <list<df[,8]>> <dbl>
## 1 Albania [57 × 8] 2708629.
## 2 Algeria [57 × 8] 24231378.
## 3 Angola [57 × 8] 11909433.
## 4 Antigua and Barbuda [57 × 8] 71053.
## 5 Argentina [57 × 8] 31638376.
## 6 Armenia [57 × 8] 2925011.
## 7 Aruba [57 × 8] 74148.
## 8 Australia [57 × 8] 16601155.
## 9 Austria [57 × 8] 7800180.
## 10 Azerbaijan [57 × 8] 6897604.
## # … with 175 more rows
瞧啊。为了好玩,这里有一张平均人口最小的 10 个国家的图表。(滚动到代码末尾)
TLDR;
结合nest
和map
创建高效的代码。然而,对于新手来说,有一些陷阱需要避免:
- 你不能不先打电话给
group_by
就打nest
。如果不这样做,代码就不知道嵌套基于哪个变量。 - 你的数据没有消失!要访问嵌套数据框中的任何内容,请调用
nested_df$data[[x]]
,其中x
是您想要查看的行。 - 嵌套数据框中的列有自己的列。是俄罗斯娃娃。
map
允许您使用函数迭代数据框的列。对于嵌套数据框,列中有列,因此可以编写.x = nested_df$data
和.f = ~some_function(.x$nested_column_inside_data_column)
。- 不要忘记你的地图函数中的波浪符号
~
!不要担心为什么(那是另一个帖子的内容),去做就是了。
绘图代码
gapminder_plot <- gapminder_nest %>%
unnest(pop_mean) %>%
select(country, pop_mean) %>%
ungroup() %>%
top_n(pop_mean, n = -10) %>%
mutate(pop_mean = pop_mean/10^3)gapminder_plot %>%
ggplot(aes(x = reorder(country, pop_mean), y = pop_mean)) +
geom_point(colour = "#FF6699", size = 5) +
geom_segment(aes(xend = country, yend = 0), colour = "#FF6699") +
geom_text(aes(label = round(pop_mean, 0)), hjust = -1) +
theme_minimal() +
labs(title = "Countries with smallest mean population from 1960 to 2016",
subtitle = "(thousands)",
x = "",
y = "") +
theme(legend.position = "none",
axis.text.x = element_blank(),
plot.title = element_text(size = 14, face = "bold"),
panel.grid.major.y = element_blank()) +
coord_flip() +
scale_y_continuous(limits = c(0, 150))
用 R: Pivot 轻松编码
pivot _ longer 和 pivot _ wider 快速指南
我不能gather
或者spread
。我有某种精神障碍,记不清哪个是哪个。我几乎总是用错,以一些奇怪的笔记本结束,沮丧地关上我的笔记本电脑。所以,感谢 R 神们最近发布的pivot_wider
和pivot_longer
。我发现它们更加直观,现在可以轻松旋转。
转动一些松鼠
Photo by Mathew Schwartz on Unsplash
为了展示 tidyverse 的新枢纽功能,我使用了来自 R4DS 在线学习社区 2019 年 10 月 29 日#TidyTuesday 挑战赛的 2019 年纽约松鼠普查数据集。
library(tidyverse)
# Load NYC Squirrel Census data set
squirrel <- read_csv("https://raw.githubusercontent.com/rfordatascience/tidytuesday/master/data/2019/2019-10-29/nyc_squirrels.csv")
我对松鼠的活动感兴趣,特别是哪种颜色的松鼠最友好。有用的变量有approaches
、indifferent
和runs_from
。这些是逻辑的,所以把它们转换成数字。也让primary_fur_color
成为一个因素。
squirrel_tidy <- squirrel %>%
select(primary_fur_color, approaches, indifferent, runs_from) %>%
mutate(primary_fur_color = fct_explicit_na(primary_fur_color, "Unknown")) %>%
mutate_if(is_logical, ~as.numeric(.))
为了找出每种颜色的松鼠接近、冷漠或逃离松鼠观察者的百分比,我用group_by
和summarise_if
搭配mean
。
friendly <- squirrel_tidy %>%
group_by(primary_fur_color) %>%
summarise_if(is.numeric, ~mean(.))## # A tibble: 4 x 4
## primary_fur_color approaches indifferent runs_from
## <fct> <dbl> <dbl> <dbl>
## 1 Black 0.0583 0.427 0.311
## 2 Cinnamon 0.112 0.462 0.222
## 3 Gray 0.0510 0.493 0.223
## 4 Unknown 0.0364 0.182 0.145
将宽数据透视为长格式
现在数据是宽格式的,可以转换为长格式了。使用pivot_longer
将活动列(approaches
、indifferent
和runs_from
)移动到一列,称为activity
。这些列中的值将被移动到一个名为pct_activity
的列中。
friendly_long <- friendly %>%
pivot_longer(cols = c("approaches", "indifferent", "runs_from"),
names_to = "activity",
values_to = "pct_activity")## # A tibble: 5 x 3
## primary_fur_color activity pct_activity
## <fct> <chr> <dbl>
## 1 Black approaches 0.0583
## 2 Black indifferent 0.427
## 3 Black runs_from 0.311
## 4 Cinnamon approaches 0.112
## 5 Cinnamon indifferent 0.462
瞧啊。整洁的数据,非常适合用ggplot
绘图。
逆转它
要从长格式转换到宽格式,可能因为你要做一些建模,所以需要专栏中的特性,使用pivot_wider
。
friendly_wide <- friendly_long %>%
pivot_wider(names_from = "activity",
values_from = "pct_activity")## # A tibble: 4 x 4
## primary_fur_color approaches indifferent runs_from
## <fct> <dbl> <dbl> <dbl>
## 1 Black 0.0583 0.427 0.311
## 2 Cinnamon 0.112 0.462 0.222
## 3 Gray 0.0510 0.493 0.223
## 4 Unknown 0.0364 0.182 0.145
但是哪种颜色的松鼠最友好呢?
由activity
刻面的pct_activity
对primary_fur_color
的绘图显示,肉桂松鼠最有可能接近(滚动到底部查看代码)。
TLDR;
pivot_longer
将多列转换成两列,创建一个整洁的数据格式。pivot_wider
则相反。
pivot_longer
需要四个输入:
data
。cols =
选中的列要变成两列。names_to =
新创建的列的名称,其中观察值将是您选择的所有列的名称。values_to =
包含所有值的新创建列的名称
pivot_wider
需要三个输入:
data
。names_from =
列名谁的观察值将成为新的列名。values_from =
列名谁的观测值将成为新的列观测值。
绘图代码
squirrel_plot_theme <- theme_minimal() +
theme(axis.title.x = element_text(size = 10),
axis.title.y = element_text(size = 10),
plot.title = element_text(size = 14, face = "bold", family = "mono"),
plot.subtitle = element_text(size = 10, face = "bold", family = "mono"),
plot.caption = element_text(size = 10, face = "italic", hjust = 0))# Create caption to correctly source all plots
squirrel_caption <- "Data from Squirrel Census 2019"# Create colour palette
squirrel_pal <- wes_palette("IsleofDogs1", 6, type = "continuous")friendly_plot <- friendly_long %>%
group_by(primary_fur_color) %>%
ggplot()+
geom_col(aes(x = activity, y = pct_activity, fill = primary_fur_color)) +
facet_wrap(~primary_fur_color) +
squirrel_plot_theme +
scale_fill_manual(values = c(squirrel_pal[4], squirrel_pal[2],
squirrel_pal[6], squirrel_pal[3]), name = "") +
scale_y_continuous(labels = percent_format(accuracy = 1), limits = c(-0.05, 0.5)) +
theme(axis.text.x = element_text(angle = 0),
panel.grid.minor = element_blank(),
legend.position = "none") +
labs(title = "Cinnamon squirrels are the friendliest",
subtitle = "Percentage of squirrels reported as approaching, being indifferent to or running from their squirrel spotter\n",
x = "",
y = "",
caption = squirrel_caption)