0%

cs231n Assignment 1

cs231n : assignment-1 笔记

作业内容:

  • knn
  • svm
  • softmax
  • two_layer_net
  • features

作业所使用数据集 :

本次作业所有算法的数据集均使用CIFAR-10,它是一个包含不同物体图片的数据集,图片大小为32*32*3

1
2
3
# Load the raw CIFAR-10 data.
cifar10_dir = 'cs231n/datasets/cifar-10-batches-py'
X_train, y_train, X_test, y_test = load_CIFAR10(cifar10_dir)

数据集中图片示例:

image


knn (k-Nearest Neighbor)

knn 也就是k近邻,它的思想很简单,也就是将训练样本中与测试样本距离最近 k 个样本看作标签的预测。当然它也可以用来做聚类,思想方法与分类一样。

距离的计算一般使用 L1 distanceL2 distance

knn 分类器训练步骤:

  1. 训练阶段:只需要将训练数据存储下来即可。
  2. 测试阶段:输入测试样本,计算它与所有训练样本之间的两两距离,取出前k个最近的训练样本,它们中最多的标记就作为测试样本的标记。

knn 需要的训练参数只有 k ,这类参数也叫做交叉验证参数(cross-validated),也就是我们在对模型进行交叉验证时,需要调整的参数。

knn 的训练过程可以看出,knn 基本没有训练时间的消耗,但是它在进行样本分类时,由于需要计算与所有训练样本间的距离,所有它的样本分类很耗时间(有一些方法可以应用来减小计算量)。

jupter notebook :

由于原始的数据太多,首先对样本进行下采样,否则算法将会非常慢。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Subsample the data for more efficient code execution in this exercise
num_training = 5000
mask = list(range(num_training))
X_train = X_train[mask]
y_train = y_train[mask]

num_test = 500
mask = list(range(num_test))
X_test = X_test[mask]
y_test = y_test[mask]

# Reshape the image data into rows
X_train = np.reshape(X_train, (X_train.shape[0], -1))
X_test = np.reshape(X_test, (X_test.shape[0], -1))
print(X_train.shape, X_test.shape)

(5000, 3072) (500, 3072)

训练部分代码:

1
2
3
4
5
# Create a kNN classifier instance. 
# Remember that training a kNN classifier is a noop:
# the Classifier simply remembers the data and does no further processing
classifier = KNearestNeighbor()
classifier.train(X_train, y_train)
1
2
3
def trian(self, X, y):
self.X_train = X
self.y_train = y

很明显,训练部分就是将训练数据存下来了而已,没有进行任何操作。

测试部分(使用 L2 distance):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Let's compare how fast the implementations are
def time_function(f, *args):
import time
tic = time.time()
f(*args)
toc = time.time()
return toc - tic

two_loop_time = time_function(classifier.compute_distances_two_loops, X_test)
print('Two loop version took %f seconds' % two_loop_time)

one_loop_time = time_function(classifier.compute_distances_one_loop, X_test)
print('One loop version took %f seconds' % one_loop_time)

no_loop_time = time_function(classifier.compute_distances_no_loops, X_test)
print('No loop version took %f seconds' % no_loop_time)
1
2
3
4
# two loop
for i in xrange(num_test):
for j in xrange(num_train):
dists[i][j] = np.sqrt(np.sum(np.square(self.X_train[j,:] - X[i,:])))
1
2
3
4
# one loop
for i in xrange(num_test):
temp = np.sqrt(np.sum(np.square(self.X_train - X[i,:]), axis = 1))
dists[i,:] = temp.T
1
2
3
4
5
6
7
# no loop
# 这里需要将 L2 的公式给写开,得到三个和式,分别计算出三部分,
# 就可以计算出距离,也就避免了显式的循环
x_square = np.sum(np.square(X), axis=1, keepdims=True)
xtr_square = np.sum(np.square(self.X_train), axis=1, keepdims=True)
two_x_y = 2*X.dot(self.X_train.T)
dists = np.sqrt((x_square + xtr_square.T) - two_x_y)

Two loop version took 20.160266 seconds
One loop version took 34.393572 seconds
No loop version took 0.665433 seconds

测试部分要求实现二重循环、一重循环和无循环三种计算距离的方式,从结果可以看出有无循环在计算时间上的巨大差异。证明在实现算法时,通过矩阵运算来直接计算的重要性。

交叉验证部分 (Cross-validation) :

由于我们可以选择不同的 k 值,所以可以使用交叉验证来测试不同k值下,knn 在数据集中的准确率,于是可以选择出最佳的 k 值。 对每一个k值,不能只进行一次测试,误差可能导致结果选择不准确,所以使用5折交叉验证。将训练数据分成五等份(随机划分),每次使用一份作为验证集,其余的作为训练集,进行五次计算,平均值作为其最终的准确率。

image

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
num_folds = 5
k_choices = [1, 3, 5, 8, 10, 12, 15, 20, 50, 100]

for k in k_choices:
this_k_cross = []
for j in range(num_folds):
xtf_copy = X_train_folds.copy()
ytf_copy = y_train_folds.copy()
del(xtf_copy[j])
del(ytf_copy[j])
xtf_train = np.vstack(xtf_copy)
ytf_train = np.hstack(ytf_copy)
xtf_val = X_train_folds[j]
ytf_val = y_train_folds[j]
classifier.train(xtf_train, ytf_train)
cross_idx = num_folds - 1
if cross_idx == j :
cross_idx = 0
y_pred = classifier.predict(xtf_val, k)
num_correct = np.sum(ytf_val == y_pred)
accuracy = float(num_correct) / each_num
this_k_cross.append(accuracy)
k_to_accuracies[k] = this_k_cross

画出的图如下:\ image

1
2
3
4
5
6
7
8
9
10
11
# 由图中看出最优的k值在10左右,这里选取10
best_k = 10

classifier = KNearestNeighbor()
classifier.train(X_train, y_train)
y_test_pred = classifier.predict(X_test, k=best_k)

# Compute and display the accuracy
num_correct = np.sum(y_test_pred == y_test)
accuracy = float(num_correct) / num_test
print('Got %d / %d correct => accuracy: %f' % (num_correct, num_test, accuracy))

Got 141 / 500 correct => accuracy: 0.282000

得到在测试集上的准确率为28.2%。

knn小结 :

knn 算法十分简单,主要涉及的就是距离的计算,所以它不能应对较为复杂的分类任务。\ 最后得到的准确率30%不到,效果很不好,还要注意到本来就能有10%的准确率,因为一共有10个分类,所以随机划分的准确率就是10%。


svm (Support Vector Machine)

SVM,也就是支持向量机,是一个很常用的算法,也是一个很有效的算法,在分类任务中经常会使用到它。\ 具体SVM的一些性质暂时先不管,因为它拥有很多的形式,也有很多的推导,加上它还有核函数的trick,所以算法实际上有很多的内容。但在这里就简单的使用即可。

初始定义 :

其中 Li 就是损失函数,首先这里我们判断一个检测样本的划分是看 f 函数得出的值最大的那一维,所以这里损失函数表达的是其它类的得分与正确类的得分之间的差值,并且差距至少为 Delta。这个损失函数一般称为 hinge loss

加入Regularization :

如果只用上面的损失函数来进行计算,它对权值 W 的取值大小是不敏感的,也就是 W 取值很大或很小时都能得到相同的结果,加入正则化,可以迫使 W 取较小的值,这里当然也有一定防止过拟合的作用。

正则函数定义:

正则化后的损失函数就是上面的 L , 在交叉验证中,Delta 的选取其实并不十分重要(一般等于1.0),因为算法总是能够适应过来,更为重要的是正则化强度 lambda 的选取。

jupyter notebook :

数据预处理 :

这里数据集选取的大小就不像 knn 这么保守,

1
2
3
4
5
# As a sanity check, print out the shapes of the data
print('Training data shape: ', X_train.shape)
print('Validation data shape: ', X_val.shape)
print('Test data shape: ', X_test.shape)
print('dev data shape: ', X_dev.shape)

Training data shape: (49000, 3072)
Validation data shape: (1000, 3072)
Test data shape: (1000, 3072)
dev data shape: (500, 3072)

这里将训练数据划分为训练本分,交叉验证部分、测试部分和快速检测部分四个部分,其中训练部分数据远远大于剩下的部分,与通常所说的70%、20%、10% 不同,也许在数据较多的时候,只要保证验证集和测试集数据能够正确检验模型即可。其中的快速检测部分用来检验代码的正常性,首先利用它来证明算法的编写没有问题,然后再开始正常的表演。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# first: compute the image mean based on the training data
mean_image = np.mean(X_train, axis=0)

# second: subtract the mean image from train and test data
X_train -= mean_image
X_val -= mean_image
X_test -= mean_image
X_dev -= mean_image

# third: append the bias dimension of ones (i.e. bias trick) so that our SVM
# only has to worry about optimizing a single weight matrix W.
X_train = np.hstack([X_train, np.ones((X_train.shape[0], 1))])
X_val = np.hstack([X_val, np.ones((X_val.shape[0], 1))])
X_test = np.hstack([X_test, np.ones((X_test.shape[0], 1))])
X_dev = np.hstack([X_dev, np.ones((X_dev.shape[0], 1))])

print(X_train.shape, X_val.shape, X_test.shape, X_dev.shape)

(49000, 3073) (1000, 3073) (1000, 3073) (500, 3073)

这里首先进行了数据的零均值化,第二步使用了加入 biastirck ,也就是在训练数据中增加一维数据,值全部为1,这样我们就可以将 bias 并入权重里面,于是在计算时就无需再单独去考虑 bias

损失函数与梯度 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def svm_loss_naive(W, X, y, reg):
num_classes = W.shape[1]
num_train = X.shape[0]
loss = 0.0
for i in xrange(num_train):
scores = X[i].dot(W)
correct_class_score = scores[y[i]]
for j in xrange(num_classes):
if j == y[i]:
continue
margin = scores[j] - correct_class_score + 1 # note delta = 1
if margin > 0:
loss += margin
dW[:,j] += X[i]
dW[:,y[i]] += -X[i]

loss /= num_train
loss += 0.5 * reg * np.sum(W * W)

dW /= num_train
dW += reg * W

首先是简单粗暴的循环版本,注意到在 max 函数的梯度计算时,对于为0的部分不计算梯度,只有非零的时候才去计算。在非零时,有两组权值加入了运算,正确分类的权值当前分类的权值,所以这两组权值都会求得梯度。如下所示:

上面的式子,就对应了上面代码中 margin > 0 的部分,调整了 dW 的第 j 列和 y{i} 列。

第二步是尝试去除显式的循环:

1
2
3
4
5
6
7
8
9
10
11
12
13
# computer loss
m = X.shape[0]
scores = X.dot(W)
right_scores = scores[range(m), y].reshape(m, 1)
temp_loss = np.maximum(0, scores - right_scores + 1)
temp_loss[range(m), y] = 0
loss = np.sum(temp_loss)/m + 0.5 * reg * np.sum(W * W)

# computer gradient
temp_loss[temp_loss > 0] = 1
row_sum = np.sum(temp_loss, axis = 1)
temp_loss[range(m), y] = -row_sum
dW += np.dot(X.T, temp_loss)/m + reg * W

loss 的向量化计算并不复杂,将得分中的正确得分置零,再求和即可。

dW 的向量化就不这么好想(这里想了很久才做出来),

  • 对于 temp_loss 中的每一行,就代表了一个样本 x[i]W 矩阵相乘,再与 0maximum 所得到的取值。
  • 单看 temp_loss 一行,在求取梯度之后,一个 j列 不为零的值,导致 dWj 列需要加上一个 x[i] , y[i] 列则需要减去 x[i] , 所以 row_sum 就计算了需要调整的次数。
  • 最后利用矩阵乘法的 trick ,注意乘出来的维度要与 dW 一致。
1
2
3
4
5
6
7
8
9
10
# compare time_use 
tic = time.time()
_, grad_naive = svm_loss_naive(W, X_dev, y_dev, 0.000005)
toc = time.time()
print('Naive loss and gradient: computed in %fs' % (toc - tic))

tic = time.time()
_, grad_vectorized = svm_loss_vectorized(W, X_dev, y_dev, 0.000005)
toc = time.time()
print('Vectorized loss and gradient: computed in %fs' % (toc - tic))

Naive loss and gradient: computed in 0.097084s
Vectorized loss and gradient: computed in 0.004936s

很明显,向量化之后效率大大提升。

训练部分(随机梯度下降) :

通过上面的验证,保证了代码的正确性,开始使用训练集进行训练。

这里所说的是使用随机梯度下降(SGD, Stochastic Gradient Descent),但事实上这里其实使用的是 mini-batch 每次随机选出部分样本,进行迭代与权重的调整。

1
2
3
4
5
6
7
8
9
def train(self, X, y, learning_rate=1e-3, reg=1e-5, num_iters=100,
batch_size=200, verbose=False):
for it in xrange(num_iters):
idxs = np.random.choice(num_train, batch_size, replace = True)
X_batch = X[idxs,:]
y_batch = y[idxs]
loss, grad = self.loss(X_batch, y_batch, reg)
loss_history.append(loss)
self.W -= learning_rate * grad
1
2
3
svm = LinearSVM()
loss_hist = svm.train(X_train, y_train, learning_rate=1e-7, reg=2.5e4,
num_iters=1500, verbose=True)

画出 loss 随迭代变化的图:

image

这里的曲线十分光滑,不知道为什么,感觉在 mini-batch 下应该会有起伏才对

1
2
3
4
5
6
# Accuracy in training and validation set
y_train_pred = svm.predict(X_train)
print('training accuracy: %f' % (np.mean(y_train == y_train_pred), ))
y_val_pred = svm.predict(X_val)
print('validation accuracy: %f' % (np.mean(y_val == y_val_pred), ))

training accuracy: 0.376714
validation accuracy: 0.377000

交叉验证 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
learning_rates = [1e-7, 1e-6, 5e-5]
regularization_strengths = [1e5, 2.5e4, 5e4, 1e3]

num_iters = 3000
for lr in learning_rates:
for rs in regularization_strengths:
svm = LinearSVM()
svm.train(X_train, y_train, lr, rs, num_iters)
y_train_pred = svm.predict(X_train)
y_val_pred = svm.predict(X_val)
ytr_acc = np.mean(y_train == y_train_pred)
yval_acc = np.mean(y_val == y_val_pred)
results[(lr,rs)] = (ytr_acc, yval_acc)
if yval_acc > best_val :
best_val = yval_acc
best_svm = svm

best validation accuracy achieved during cross-validation: 0.394000

这部分首先参数设置很诡异啊,学习步长非常小,正则强度又非常之大,计算时在学习步长为 5e-5 时会出现数值爆炸的情况。

取得在交叉验证集中的准确率为 39.4%

1
2
3
4
# Evaluate the best svm on test set
y_test_pred = best_svm.predict(X_test)
test_accuracy = np.mean(y_test == y_test_pred)
print('linear SVM on raw pixels final test set accuracy: %f' % test_accuracy)

linear SVM on raw pixels final test set accuracy: 0.370000

取得在测试集中的准确率为 37.0%,比 knn 只好 10% 左右。

将权值可视化,如下图所示:

image

从图像中可以看出图中大概的形状以及背景颜色与它所要区分的类别很相近。个人认为由于首先零均值化之后,数据中有正有负,然后这里 SVM 只涉及到乘法,当权值与样本数据接近时,得到的得分就会比较高,也是因为这里太简单了,所以导致效果并不是很好。

SVM小结 :

这里使用了多分类的SVM,可以看出效果并不很好,事实上,可以使用 one-vs-all 的方式或者使用核函数来进行分类,相信会得到更好的结果。


softmax classifier

softmax classifier 也是一种常用的分类器,它其实长得很像 Logistic Regression ,下面是它的损失函数的定义:

其中 W 为权值,xi 为第 i 个样本。可以把它写开成为等价形式,

这样的损失函数称为交叉熵损失(cross-entropy loss),其中,

就是 softmax function。从直观来说,softmax function 把得分转化为了概率,然后用类似熵的式子来使得正确分类的概率最大化(因为要最小化损失函数)。

在实际计算中,如果不对分数进行处理就直接带入损失函数,由于其中涉及到 e 的幂次,可能导致计算上数值过大造成问题,所以需要对其做一些处理,如下,

这样就相当于要所有得分共同加上一个固定值 logC ,并且这对计算的结果不会产生影响。于是,通常情况下我们取,

这样就不会带来计算上溢出的问题,同时也不会影响到计算结果,所以也不用花费额外的精力去关注它。

加入正则化(与 SVM 一样),写出最终的损失函数如下,

jupyter notebook :

数据预处理 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# Load the raw CIFAR-10 data
X_train, y_train, X_test, y_test = load_CIFAR10(cifar10_dir)

# Preprocessing: reshape the image data into rows
X_train = np.reshape(X_train, (X_train.shape[0], -1))
X_val = np.reshape(X_val, (X_val.shape[0], -1))
X_test = np.reshape(X_test, (X_test.shape[0], -1))
X_dev = np.reshape(X_dev, (X_dev.shape[0], -1))

# Normalize the data: subtract the mean image
mean_image = np.mean(X_train, axis = 0)
X_train -= mean_image
X_val -= mean_image
X_test -= mean_image
X_dev -= mean_image

# add bias dimension and transform into columns
X_train = np.hstack([X_train, np.ones((X_train.shape[0], 1))])
X_val = np.hstack([X_val, np.ones((X_val.shape[0], 1))])
X_test = np.hstack([X_test, np.ones((X_test.shape[0], 1))])
X_dev = np.hstack([X_dev, np.ones((X_dev.shape[0], 1))])

print('Train data shape: ', X_train.shape)
print('Train labels shape: ', y_train.shape)
print('Validation data shape: ', X_val.shape)
print('Validation labels shape: ', y_val.shape)
print('Test data shape: ', X_test.shape)
print('Test labels shape: ', y_test.shape)
print('dev data shape: ', X_dev.shape)
print('dev labels shape: ', y_dev.shape)

Train data shape: (49000, 3073)
Train labels shape: (49000,)
Validation data shape: (1000, 3073)
Validation labels shape: (1000,)
Test data shape: (1000, 3073)
Test labels shape: (1000,)
dev data shape: (500, 3073)
dev labels shape: (500,)

这里使用的数据同样是 CIFAR10

  • 代码首先读入数据,并将每一幅图片都转化为行向量。
  • 将数据划分为四部分,和 SVM 时操作一样。
  • 零均值化。
  • 使用 bias trick,加入一列 1 向量。

损失函数和梯度 :

首先实现循环版本的代码,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
m = X.shape[0]
c = W.shape[1]
for i in xrange(m) :
score = X[i].dot(W)
score_mean = score - np.max(score)
score_exp = np.exp(score_mean)
sum_se = np.sum(score_exp)
loss += (np.log(sum_se) - score_mean[y[i]])
for j in xrange(c):
dW[:,j] += (score_exp[j] / sum_se) * X[i]
if j == y[i]:
dW[:,j] -= X[i]
loss /= m
loss += 0.5 * reg * np.sum(W * W)
dW /= m
dW += reg * W

循环中,将计算分为了每个样本分别计算,利用了损失函数的变形来编写,其中,

循环中就体现了上面的式子,在最后再加入正则化即可。

1
2
3
4
5
6
7
# Generate a random softmax weight matrix and use it to compute the loss.
W = np.random.randn(3073, 10) * 0.0001
loss, grad = softmax_loss_naive(W, X_dev, y_dev, 0.0)

# As a rough sanity check, our loss should be something close to -log(0.1).
print('loss: %f' % loss)
print('sanity check: %f' % (-np.log(0.1)))

loss: 2.367492
sanity check: 2.302585

上面这一段使用了随机的初始权值来对 loss 进行了计算,然后与 -log(0.1) 进行了比较,在期望下两个值之间应该十分接近,因为在随机情况下,每一个样本在 softmax 之后它得每一类划分的概率应该都是 0.1,在取熵之后就是接近 -log(0.1)

梯度检测 :

这部分留到最后在统一说,通常在算法实现时,我们要先检查自己的梯度计算是否有问题,以保证在正常训练时不会出现问题。

向量化 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# loss
m = X.shape[0]
scores = X.dot(W)
scores_mean = scores - np.max(scores, axis = 1).reshape(m, 1)
scores_exp = np.exp(scores_mean)
sum_row = np.sum(scores_exp, 1)
loss += -np.sum(np.log(scores_exp[range(m), y]/sum_row))

# gradient
coef = scores_exp / sum_row.reshape(m,1)
coef[np.arange(m), y] -= 1
dW = X.T.dot(coef)

# regularization
loss /= m
loss += 0.5 * reg * np.sum(W * W)
dW /= m
dW += reg * W

在经历过 SVM 的向量化之后,这里的向量化就和它有一定类似,只不过 SVM 时,系数为 1 ,在这里系数是概率,外加正确值减去一个 -1,然后同样使用矩阵乘法所得结果需要与 dW 的规模一致的 trick,得到代码的机构。

1
2
3
4
5
6
7
8
9
tic = time.time()
loss_naive, grad_naive = softmax_loss_naive(W, X_dev, y_dev, 0.000005)
toc = time.time()
print('naive loss: %e computed in %fs' % (loss_naive, toc - tic))

tic = time.time()
loss_vectorized, grad_vectorized = softmax_loss_vectorized(W, X_dev, y_dev, 0.000005)
toc = time.time()
print('vectorized loss: %e computed in %fs' % (loss_vectorized, toc - tic))

naive loss: 2.349745e+00 computed in 0.113950s
vectorized loss: 2.349745e+00 computed in 0.011255s

两次计算的结果一致,向量化的速度是非向量化的 10 倍。

交叉验证 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
learning_rates = [1e-7, 5e-7]
regularization_strengths = [2.5e4, 5e4]

it_num = 1500
for lr in learning_rates :
for rs in regularization_strengths :
softmax = Softmax()
softmax.train(X_train, y_train, lr, rs, it_num)
y_train_pred = softmax.predict(X_train)
y_val_pred = softmax.predict(X_val)
ytr_acc = np.mean(y_train == y_train_pred)
yval_acc = np.mean(y_val == y_val_pred)
results[(lr,rs)] = (ytr_acc, yval_acc)
if yval_acc > best_val :
best_val = yval_acc
best_softmax =

print('best validation accuracy achieved during cross-validation: %f' % best_val)

best validation accuracy achieved during cross-validation: 0.372000

得到交叉验证集中的最高准确率为 37.2%

1
2
3
4
# Evaluate the best softmax on test set
y_test_pred = best_softmax.predict(X_test)
test_accuracy = np.mean(y_test == y_test_pred)
print('softmax on raw pixels final test set accuracy: %f' % (test_accuracy, ))

softmax on raw pixels final test set accuracy: 0.369000

得到在测试集中的准确率为 36.9%

将权值可视化之后得到图如下,

image

得到的图像与之前的 SVM 几乎是一样的,因为在实际上它们所观察的,都是在 W*xi 中的最大取值,它们之间的学习步长以及正则化强度的取值大小几乎也是一样的,所以最终得到的准确率几乎也一样。

softmax小结 :

softmax 的函数形式就可以看出,它适合的是多分类的场景,但它在 CIFAR10 上的表现并不好,说明在这种过于复杂的任务中,它很难轻易的表现的很好,可能还需要对数据进行一些特征提取的工作,来提升它的性能。


Two_Layer_Neural_Network

这里要求实现一个简单的二层神经网络(也可以称作一个隐藏层的神经网络),神经网络的东西单独写,先写题。

image

如图所示就类似于题目要求的神经网络,输入层 + 隐藏层 + 输出层,完成多分类任务。更为具体的,题目要求网络结构为,

  • input - fully connected layer - ReLU - fully connected layer - softmax

jupyter notebook :

前向传播(Forward Pass) :

1
2
3
# Forward Pass
h = np.maximum(0, X.dot(W1) + b1)
scores = h.dot(W2) + b2

前向传播十分简单,两个矩阵乘法就能搞定。

损失函数(loss function) :

1
2
3
4
5
6
7
8
# loss
scores_mean = scores - np.max(scores, 1, keepdims=True)
scores_exp = np.exp(scores_mean)
sc_sum = np.sum(scores_exp, 1, keepdims=True)
y_pred = scores_exp / sc_sum
loss = -np.sum(np.log(y_pred[range(N), y]))
loss /= N
loss += reg * (np.sum(W1 * W1) + np.sum(W2 * W2))

注意这里的损失函数是 softmax

反向传播(Backward Pass) :

1
2
3
4
5
6
7
8
9
10
11
12
# Backward Pass
dscores = y_pred.copy()
dscores[range(N), y] -= 1
dscores /= N
grads['W2'] = h.T.dot(dscores) + 2 * reg * W2
grads['b2'] = np.sum(dscores, axis = 0)

h_grad = dscores.dot(W2.T)
h_grad[h <= 0] = 0
grads['W1'] = X.T.dot(h_grad) + 2 * reg * W1
grads['b1'] = np.sum(h_grad, axis = 0)

这里写了很多遍也不对,对照别人的代码才知道这里的误差起始直接用的就是 y_pred ,也就是说正确类的误差为 1 - fj ,错误类的误差为 fj - 0,也就是说我们期望正确类的概率为 1 ,其余为 0, 不用先对 softmax 求导再传播。另外这里的误差还要除以样本数 N没想明白???

另外反向传播需要用到前向传播的中间值,这里就是 h, y_pred

反向传播在书写代码时,利用矩阵乘法的 trick 可以迅速梳理出写法。

  • 第一次传播就直接回传
  • 第二次传播是 hinge function ,它是个不连续的函数,我们处理它的反向传播时,将前向传播时为 0 的值,不再反向传播即可。
  • 加上正则化

训练部分 :

1
2
3
4
5
6
7
8
9
10
11
12
13
# mini-batch
idx = np.random.choice(num_train, batch_size, replace=True)
X_batch = X[idx]
y_batch = y[idx]

# loss, grads
loss, grads = self.loss(X_batch, y=y_batch, reg=reg)

# weight update
self.params['W1'] -= learning_rate * grads['W1']
self.params['b1'] -= learning_rate * grads['b1']
self.params['W2'] -= learning_rate * grads['W2']
self.params['b2'] -= learning_rate * grads['b2']

使用mini-batch的方法来训练,权值更新十分简单,训练过程如下图,

image

图像中 loss 的变化是一直在跳变的,但是它总体的趋势是在下降,这就是在训练中使用 batch model 以外的训练方法中会看到的场景,因为每次我们沿梯度下降时,寻找的是本次使用样本的梯度下降方向,而不是全局的下降方向。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
input_size = 32 * 32 * 3
hidden_size = 50
num_classes = 10
net = TwoLayerNet(input_size, hidden_size, num_classes)

# Train the network
stats = net.train(X_train, y_train, X_val, y_val,
num_iters=1000, batch_size=200,
learning_rate=1e-4, learning_rate_decay=0.95,
reg=0.25, verbose=True)

# Predict on the validation set
val_acc = (net.predict(X_val) == y_val).mean()
print('Validation accuracy: ', val_acc)

Validation accuracy: 0.287

在上面定义了一个隐藏层为 50 个神经元的一个神经网络,这里只迭代 1000 次,得到一个粗略的 Validation accuracy28.7% ,感觉很差。所以下面要进行交叉验证来找到一个好的参数,神经网络时常干的就是这么一个调参的工作。

先看当前训练得到权值的一个长相,

image

只有车的长相比较明显。

交叉验证 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# cross-validation
hidden_size = [ 100, 200, 300, 400, 500 ]
learning_rate = [ 1e-3, 1e-4]
reg = [ 0.15, 0.25, 0.45]

for hs in hidden_size:
for lr in learning_rate:
for re in reg:
net = TwoLayerNet(input_size, hs, num_classes)
# Train the network
stats = net.train(X_train, y_train, X_val, y_val,
num_iters=4000, batch_size=400,
learning_rate=lr, learning_rate_decay=0.99,
reg=re, verbose=True)
# Predict on the validation set
val_acc = (net.predict(X_val) == y_val).mean()
print('Validation accuracy: ', val_acc, ' hs = ',
hs, ' lr = ', lr, ' re = ', re)
if val_acc > best_val:
best_val = val_acc
best_net = net

Validation accuracy: 0.528 hs = 500 lr = 0.0005 re = 0.2

这里的训练过程非常慢,事实上我看效果不好的参数就主动停止了它。已经可以预见在网络更为复杂的时候,光用 cpu 来跑神经网络的困难所在了。

这里得到了 52.8% 的验证集准确率。

将这个复杂的网络权值进行可视化,

image

图中还是可以依稀辨识出形状的。

1
2
test_acc = (best_net.predict(X_test) == y_test).mean()
print('Test accuracy: ', test_acc)

Test accuracy: 0.546

得到最后在测试集上的准确率为 54.6% ,实际上我在测试时跑出的最高准确率为 56% ,再调整了一下参数,效果反而差了一点(;′⌒`) 。

two_layer_net 小结 :

对于神经网络,还有很多神奇的操作现在还不明白,但是很明显它在这种复杂问题上的分类能力是要强于其它的分类方法的,起码在这里比 SVM、Softmax20%


features :

这一部分使用了原始图像的方向梯度直方图(HOG,Histogram of Oriented Gradient)的特征来进行训练,具体它是怎么完成的现在并不关心,总之它提取了特征,不再使用图像的原始特征,然后能够提高算法的准确率。

特征与数据预处理 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
num_color_bins = 10 # Number of bins in the color histogram
feature_fns = [hog_feature, lambda img: color_histogram_hsv(img, nbin=num_color_bins)]
X_train_feats = extract_features(X_train, feature_fns, verbose=True)
X_val_feats = extract_features(X_val, feature_fns)
X_test_feats = extract_features(X_test, feature_fns)

# Preprocessing: Subtract the mean feature
mean_feat = np.mean(X_train_feats, axis=0, keepdims=True)
X_train_feats -= mean_feat
X_val_feats -= mean_feat
X_test_feats -= mean_feat

# Preprocessing: Divide by standard deviation. This ensures that each feature
# has roughly the same scale.
std_feat = np.std(X_train_feats, axis=0, keepdims=True)
X_train_feats /= std_feat
X_val_feats /= std_feat
X_test_feats /= std_feat

# Preprocessing: Add a bias dimension
X_train_feats = np.hstack([X_train_feats, np.ones((X_train_feats.shape[0], 1))])
X_val_feats = np.hstack([X_val_feats, np.ones((X_val_feats.shape[0], 1))])
X_test_feats = np.hstack([X_test_feats, np.ones((X_test_feats.shape[0], 1))])

上面的过程完成了以下操作:

  • 特征提取
  • 零均值化
  • 单位化方差
  • 加入偏置

此时数据的维度发生了很大的变化,

1
print(X_train_feats.shape)

(49000, 155)

数据直接由 3072 维,变到了 154 维(不考虑偏置维),算法复杂度也会显著降低。

训练SVM :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# cross-validation
learning_rates = [1e-9, 1e-8, 1e-7]
regularization_strengths = [5e4, 5e5, 5e6]

num_iters = 3000
for lr in learning_rates:
for rs in regularization_strengths:
svm = LinearSVM()
svm.train(X_train_feats, y_train, lr, rs, num_iters)
y_train_pred = svm.predict(X_train_feats)
y_val_pred = svm.predict(X_val_feats)
ytr_acc = np.mean(y_train == y_train_pred)
yval_acc = np.mean(y_val == y_val_pred)
results[(lr,rs)] = (ytr_acc, yval_acc)
if yval_acc > best_val :
best_val = yval_acc
best_svm = svm

print('best validation accuracy achieved during cross-validation: %f' % best_val)

best validation accuracy achieved during cross-validation: 0.425000

1
2
3
4
# Evaluate your trained SVM on the test set
y_test_pred = best_svm.predict(X_test_feats)
test_accuracy = np.mean(y_test == y_test_pred)
print(test_accuracy)

0.424

得到测试集准确率为 42.4%,比之前提升了 5% 左右。

来观察一下它的误分类的长相,

image

恩,没什么规律可言O__O “…

训练神经网络 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# cross-validation
hidden_dim = 400

hidden_size = [ hidden_dim, ]
learning_rate = [0.2, 0.25, 0.30 ]
reg = [ 0.00005, 0.00001, 0.000005 ]

for hs in hidden_size:
for lr in learning_rate:
for re in reg:
net = TwoLayerNet(input_dim, hs, num_classes)
# Train the network
stats = net.train(X_train_feats, y_train, X_val_feats, y_val,
num_iters=4000, batch_size=400,
learning_rate=lr, learning_rate_decay=0.98,
reg=re, verbose=False)
# Predict on the validation set
val_acc = (net.predict(X_val_feats) == y_val).mean()
print('Validation accuracy: ', val_acc, ' hs = ',
hs, ' lr = ', lr, ' re = ', re)
if val_acc > best_val:
best_val = val_acc
best_net = net

Validation accuracy: 0.63 hs = 400 lr = 0.2 re = 5e-05
Validation accuracy: 0.587 hs = 400 lr = 0.2 re = 1e-05
Validation accuracy: 0.58 hs = 400 lr = 0.2 re = 5e-06
Validation accuracy: 0.583 hs = 400 lr = 0.25 re = 5e-05
Validation accuracy: 0.582 hs = 400 lr = 0.25 re = 1e-05
Validation accuracy: 0.587 hs = 400 lr = 0.25 re = 5e-06
Validation accuracy: 0.593 hs = 400 lr = 0.3 re = 5e-05
Validation accuracy: 0.576 hs = 400 lr = 0.3 re = 1e-05
Validation accuracy: 0.588 hs = 400 lr = 0.3 re = 5e-06

1
2
test_acc = (net.predict(X_test_feats) == y_test).mean()
print(test_acc)

0.592

这里得到测试集准确率为 59.2% ,比之前提高了 4% ,虽然提升不多,但是模型复杂度也降低了,所以训练速度比之前快了很多。

特征提取小结:

在没有广泛应用神经网络的时期,特征提取和数据的预处理是机器学习中非常重要的一部分,甚至是主要的一部分,神经网络的广泛应用,其实就将特征提取这个部分给简单化了,模型只带特征提取的能力。

具体的很多特征提取的方法目前还不知道,随着课程推进再慢慢学习。

第一部分的笔记就先到这里,这算是我的第一次笔记!!!