理论知识
这次的分类器要比之前的KNN更加得灵活,效果也会更好。这种方法主要有两部分组成:一个是评分函数(score function),它是原始图像数据到类别分值的映射。另一个是损失函数(loss function),它是用来量化预测分类标签的得分与真实标签之间一致性的。该方法可转化为一个最优化问题,在最优化过程中,将通过更新评分函数的参数来最小化损失函数值。
评分函数可以表示为:
对于
当然更好的方法是把W和b合并:
评分函数在正确的分类的位置应当得到最高的评分(score)。使用损失函数(Loss Function)(有时也叫代价函数Cost Function或目标函数Objective)来衡量我们对结果的不满意程度。直观地讲,当评分函数输出结果与真实结果之间差异越大,损失函数输出越大,反之越小。
损失函数的形式很多,这里使用的是SVM损失函数。
对于第j个类别的得分为
SVM的损失函数想要正确分类类别
那么根据之前的评分函数,最终的损失函数为:
正则化(Regularization):课程中是这么解释的,由于使所有样本都能正确分类的
正则化的想法就是向某些特定的权重W添加一些偏好,对其他权重则不添加,以此来消除这种模糊性。常用的正则化惩罚是L2范数:
可以看出,对于比较大的权重,它可以使其损失函数更大,从而抑制大数值的权重。
我的理解是:在训练完后发现了过拟合,那么此时虽然训练集的损失很小了,但是还不是最好的W,所以要调整W,W还有偏好的含义,越大表示对某个特征反应越激烈,而较大的W在预测中往往有着决定性的作用,所以要降低这些权重,对大数值进行惩罚,来让分类器把所有维度上的特征都利用起来,从而提升其泛化能力。
最后的loss如下:
算法实现
整个线性分类器的设计过程总结如下:
- 对数据集进行预处理。包括划分训练集,验证集和测试集。零均值化和归一化。
- 设计损失函数
- 计算梯度和loss
- 设计优化算法进行权重更新
- 训练
- 调优
算法框架如下:
class LinearClassifier(object):
def __init__(self):
self.W = None
def train(self, X, y, learning_rate=1e-3, reg=1e-5, num_iters=100,
batch_size=200, verbose=False):
pass
return loss_history
def predict(self, X):
pass
return y_pred
def loss(self, X_batch, y_batch, reg):
pass
class LinearSVM(LinearClassifier):
""" A subclass that uses the Multiclass SVM loss function """
def loss(self, X_batch, y_batch, reg):
return svm_loss_vectorized(self.W, X_batch, y_batch, reg)
因为不同损失函数的train和predict的代码是相同的,所以作业中使用了继承来复用这些代码,以后只需要写loss的代码即可。
train代码实现:
def train(self, X, y, learning_rate=1e-3, reg=1e-5, num_iters=100,
batch_size=200, verbose=False):
num_train, dim = X.shape
num_classes = np.max(y) + 1 # 这里用 0...K-1 来表示不同的标签
#权重初始化,使用小随机数
if self.W is None:
# lazily initialize W
self.W = 0.001 * np.random.randn(dim, num_classes)
#使用GD进行W的优化
loss_history = []
for it in range(num_iters):
X_batch = None
y_batch = None
num_train = X.shape[0]
batch_indices = np.random.choice(num_train, batch_size)
X_batch = X[batch_indices]
y_batch = y[batch_indices]
loss, grad = self.loss(X_batch, y_batch, reg)
loss_history.append(loss)
self.W -=learning_rate*grad
if verbose and it % 100 == 0:#用于观察loss的情况
print('iteration %d / %d: loss %f' % (it, num_iters, loss))
return loss_history
这里说明下batch_size,epoch的区别。
当一个完整的数据集通过了分类器一次并且完成了一次梯度更新,这个过程称为一个epoch。然而,当一个 epoch 对于计算机而言太庞大的时候,就需要把它分成多个小块。分成的块数就是batch的数量,每一个batch的大小就为batchsize。现在变成了每一个batch进行一次梯度更新。batch_size和梯度下降也有一定关系:
批量梯度下降(BGD)。batch_size=训练集的大小
随机梯度下降(SGB)。batch_size= 1
小批量梯度下降(MBGD)。1 <batch_size<训练集的大小
predict的代码实现:
def predict(self, X):
y_pred = np.zeros(X.shape[0])
y_pred = np.argmax(X.dot(self.W),axis=1)
return y_pred
svm_loss_vectorized的实现:
def svm_loss_vectorized(W, X, y, reg):
loss = 0.0
dW = np.zeros(W.shape) # initialize the gradient as zero
scores = X.dot(W) # N by C
num_train = X.shape[0]
num_classes = W.shape[1]
#计算loss
#numpy的这种技巧要记牢啊
scores_correct = scores[np.arange(num_train), y] #1 by N
scores_correct = np.reshape(scores_correct, (num_train, 1)) # N by 1
margins = scores - scores_correct + 1.0 # N by C
margins[np.arange(num_train), y] = 0.0
margins[margins <= 0] = 0.0
loss += np.sum(margins) / num_train
loss += 0.5 * reg * np.sum(W * W)
#计算梯度
margins[margins > 0] = 1.0 # 示性函数的意义
row_sum = np.sum(margins, axis=1) # 1 by N
margins[np.arange(num_train), y] = -row_sum
dW += np.dot(X.T, margins)/num_train + reg * W # D by C
return loss, dW
(关于loss和梯度计算的实现想专门总结下)
训练集和验证集的正确率为:
调优
这次直接使用验证集来进行调整学习率和正则化系数,具体代码如下:
learning_rates = [1e-7,5e-5]
regularization_strengths = [2.5e4,5e4]
#对于每一个超参数集合,用训练集训练一个SVM,计算训练集和验证集的准确率,并存放在
#result字典中,存放最好的验证集准确率在best_val中,最好的模型放在best_svm中
#result中是(learning_rate, regularization_strength):(training_accuracy, validation_accuracy)
results = {}
best_val = -1
best_svm = None
for lr in learning_rates:
for rs in regularization_strengths:
svm = LinearSVM()
loss_hist = svm.train(X_train, y_train, learning_rate=lr, reg=rs,
num_iters=500, verbose=False)#num_iters设为500,调高查找速度
y_train_pred = svm.predict(X_train)
train_accuracy = np.mean(y_train == y_train_pred)
y_val_pred = svm.predict(X_val)
validation_accuracy = np.mean(y_val == y_val_pred)
results[(lr,rs)] = (train_accuracy,validation_accuracy)
if best_val < validation_accuracy:
best_svm = svm
best_val = validation_accuracy
# Print out results.
for lr, reg in sorted(results):
train_accuracy, val_accuracy = results[(lr, reg)]
print('lr %e reg %e train accuracy: %f val accuracy: %f' % (
lr, reg, train_accuracy, val_accuracy))
print('best validation accuracy achieved during cross-validation: %f' % best_val)
正确率与超参数的关系如下:
左上方的颜色为深红色,表明准确率最高。学习率为1e-7 ,正则化系数为 5e4,验证集上的准确率为38%左右。
之后要注意的是在对测试集进行预测之前,要把num_iters调到较大的值,这里是1500,来更好地优化W。最后测试集的准确率为37%左右。
作业中还把权重矩阵中各个类别的权重绘图,结果如下:
可以发现,SVM分类器是在进行着模板匹配的工作。
一些问题:
1.多类SVM损失函数的最大/最小值是多少?
最小值:0 最大值:无穷大
2.如果初始化时w和b很小,损失L会是多少?
设标签数为n,单个样本的L为n-1。这可以验证编码是否正确
3.考虑所有类别(包括j=yi),损失Li会有什么变化?
会比原来多1
4.在求总损失L计算时,如果用求和代替平均?
没有什么影响,可以调学习率
5.如果使用
会使损失值变很大,对最终的效果有影响。