準教師学習とは一部のデータにのみ正解ラベルがつけられていて、残りはラベルがないという状態で、学習を行うことです。
ディープラーニングに必要な1クラスあたり数千のラベルを用意するのは大変なので、準教師学習の応用範囲は広いと考えています。
全てのデータセットにラベルがついてる場合でも準教師学習の手法を導入することによって性能が上がる場合があります。
今回はVAT(Virtual Adversarial Training)を使ってみます。
なぜVAT?
導入が非常に楽で、そこそこ性能がいいからです。性能だけならLadder Networkの方が上です。
ここではmattyaさんの実装
chainer-semi-supervised/vat.py at master · mattya/chainer-semi-supervised · GitHub
のパクってみます。
が、trainerを使ってみます。
前準備(必要な関数の宣言)
パクリ元から持ってきます。
class KL_multinominal(chainer.function.Function):
""" KL divergence between multinominal distributions """
def __init__(self):
pass
def forward_gpu(self, inputs):
p, q = inputs
loss = cuda.cupy.ReductionKernel(
'T p, T q',
'T loss',
'p*(log(p)-log(q))',
'a + b',
'loss = a',
'0',
'kl'
)(p, q)
return loss / numpy.float32(p.shape[0]),
def backward_gpu(self, inputs, grads):
p, q = inputs
dq = -numpy.float32(1.0) * p / (np.float32(1e-8) + q) / numpy.float32(
p.shape[0]) * grads[0]
return cuda.cupy.zeros_like(p), dq
def kl(p, q):
return KL_multinominal()(F.softmax(p), F.softmax(q))
def kl(p, q):
return KL_multinominal()(p, q)
def distance(y0, y1):
return kl(F.softmax(y0), F.softmax(y1))
def vat(model, distance, x, xi=10, eps=1.0, Ip=1):
xp = cuda.cupy
y = model.predict(x)
y.unchain_backward()
d = xp.random.normal(size=x.shape, dtype=numpy.float32)
sum_axis = [i for i in range(d.ndim) if i]
shape = (x.shape[0],) + (1,) * (d.ndim - 1)
d = d / xp.sqrt(xp.sum(d ** 2, axis=sum_axis)).reshape((shape))
for ip in range(Ip):
d_var = Variable(d.astype(numpy.float32))
y2 = model.predict(x + xi * d_var)
kl_loss = distance(y, y2)
kl_loss.backward()
d = d_var.grad
d = d / xp.sqrt(xp.sum(d ** 2, axis=sum_axis)).reshape((shape))
d_var = Variable(d.astype(numpy.float32))
y2 = model.predict(x + eps * d_var)
return distance(y, y2)
classの変更
まず、VAT学習ができるクラスを用意します。
class VATLossClassifier(chainer.Chain):
def __call__(self, x, t=None):
if t is not None:
h = self.predict(x)
loss = F.softmax_cross_entropy(h, t)
chainer.report({'loss': loss, 'accuracy': F.accuracy(h, t)}, self)
return loss
else:
return vat(self, distance, x, self.eps)
alexnetにVATLossClassifierを継承させます。実際にはモデルの形に依存せずVATを適用することができます。
class Alex(VATLossClassifier):
"""Single-GPU AlexNet without partition toward the channel axis."""
insize = 227
def __init__(self):
super(Alex, self).__init__()
with self.init_scope():
self.conv1 = L.Convolution2D(None, 96, 11, stride=4)
self.conv2 = L.Convolution2D(None, 256, 5, pad=2)
self.conv3 = L.Convolution2D(None, 384, 3, pad=1)
self.conv4 = L.Convolution2D(None, 384, 3, pad=1)
self.conv5 = L.Convolution2D(None, 256, 3, pad=1)
self.fc6 = L.Linear(None, 4096)
self.fc7 = L.Linear(None, 4096)
self.fc8 = L.Linear(None, 1000)
def __call__(self, x, t):
h = F.max_pooling_2d(F.local_response_normalization(
F.relu(self.conv1(x))), 3, stride=2)
h = F.max_pooling_2d(F.local_response_normalization(
F.relu(self.conv2(h))), 3, stride=2)
h = F.relu(self.conv3(h))
h = F.relu(self.conv4(h))
h = F.max_pooling_2d(F.relu(self.conv5(h)), 3, stride=2)
h = F.dropout(F.relu(self.fc6(h)))
h = F.dropout(F.relu(self.fc7(h)))
h = self.fc8(h)
return h
alexnetを継承して、ラベルがないときにもlossが吐けるようにします。
trainerを使う場合、updaterを変更して、VAT学習に対応させます。
class VATUpdater(updater.StandardUpdater):
def update(self):
self.update_core()
self.update_vat()
self.iteration += 1
def update_vat(self):
batch = self._iterators['main'].next()
in_arrays = self.converter(batch, self.device)
optimizer = self._optimizers['main']
loss_func = optimizer.target
optimizer.zero_grads()
optimizer.update(loss_func, in_arrays[0])
optimizer.zero_grads()
以上で、準備はおしまい。
後はtrainerにVATUpdaterを食わせて通常通り、trainer.run()するだけです。
一部のデータのみラベルが存在する場合、__init__を編集して、2つのデータセットに対して別々のイテレーターを設定することで、準教師学習を実行できます。