Rumah >Peranti teknologi >AI >sukar dipercayai! Gunakan Numpy untuk membangunkan rangka kerja pembelajaran mendalam dan melihat ke dalam proses latihan rangkaian saraf
Helo, semua.
Hari ini saya ingin berkongsi dengan anda projek sumber terbuka yang sangat hebat, saya membangunkan rangka kerja pembelajaran mendalam menggunakan Numpy pada asasnya sama dengan Pytorch.
Hari ini kami mengambil rangkaian neural convolutional ringkas sebagai contoh untuk menganalisis langkah teras yang terlibat dalam proses latihan rangkaian saraf, seperti perambatan ke hadapan, perambatan belakang dan pengoptimuman parameter Kod sumber.
Set data dan kod yang digunakan telah dibungkus, dan terdapat cara untuk mendapatkannya di penghujung artikel.
Sediakan data dan kod terlebih dahulu.
Mula-mula, muat turun kod sumber rangka kerja, alamat: https://github.com/duma-repo/PyDyNet
git clone https://github.com/duma-repo/PyDyNet.git
Bina LeNet rangkaian rangkaian neural konvolusi untuk melatih model tiga klasifikasi.
Cukup buat fail kod terus dalam direktori PyDyNet.
from pydynet import nn class LeNet(nn.Module): def __init__(self): super().__init__() self.conv1 = nn.Conv2d(1, 6, kernel_size=5, padding=2) self.conv2 = nn.Conv2d(6, 16, kernel_size=5) self.avg_pool = nn.AvgPool2d(kernel_size=2, stride=2, padding=0) self.sigmoid = nn.Sigmoid() self.fc1 = nn.Linear(16 * 5 * 5, 120) self.fc2 = nn.Linear(120, 84) self.fc3 = nn.Linear(84, 3) def forward(self, x): x = self.conv1(x) x = self.sigmoid(x) x = self.avg_pool(x) x = self.conv2(x) x = self.sigmoid(x) x = self.avg_pool(x) x = x.reshape(x.shape[0], -1) x = self.fc1(x) x = self.sigmoid(x) x = self.fc2(x) x = self.sigmoid(x) x = self.fc3(x) return x
Seperti yang anda lihat, takrifan rangkaian adalah sama seperti sintaks Pytorch.
Dalam kod sumber yang saya sediakan, fungsi ringkasan disediakan untuk mencetak struktur rangkaian.
Data latihan menggunakan set data Fanshion-MNIST, yang mengandungi 10 kategori gambar, 6k imej dalam setiap kategori.
Untuk mempercepatkan latihan, saya hanya mengekstrak 3 kategori pertama, sejumlah imej latihan 1.8w, untuk membuat model tiga klasifikasi.
import pydynet from pydynet import nn from pydynet import optim lr, num_epochs = 0.9, 10 optimizer = optim.SGD(net.parameters(), lr=lr) loss = nn.CrossEntropyLoss() for epoch in range(num_epochs): net.train() for i, (X, y) in enumerate(train_iter): optimizer.zero_grad() y_hat = net(X) l = loss(y_hat, y) l.backward() optimizer.step() with pydynet.no_grad(): metric.add(l.numpy() * X.shape[0], accuracy(y_hat, y), X.shape[0])
Kod latihan juga sama seperti Pytorch.
Perkara utama yang perlu dilakukan seterusnya ialah mendalami kod sumber latihan model untuk mempelajari prinsip latihan model.
Net.train akan dipanggil sebelum model memulakan latihan.
def train(self, mode: bool = True): set_grad_enabled(mode) self.set_module_state(mode)
Seperti yang anda lihat, ia akan menetapkan grad(gradient) kepada True dan Tensor yang dibuat selepas itu boleh mempunyai kecerunan. Selepas Tensor membawa kecerunan, ia akan dimasukkan ke dalam graf pengiraan dan menunggu terbitan untuk mengira kecerunan.
Yang berikut dengan no_grad(): kod
class no_grad: def __enter__(self) -> None: self.prev = is_grad_enable() set_grad_enabled(False)
akan menetapkan grad(gradient) kepada False, supaya Tensor yang dibuat kemudian tidak akan diletakkan dalam graf pengiraan, secara semula jadi Juga tidak perlu mengira kecerunan, yang boleh mempercepatkan inferens.
Kami sering melihat penggunaan net.eval() dalam Pytorch, dan kami juga melihat kod sumbernya.
def eval(self): return self.train(False)
Seperti yang anda lihat, ia secara terus memanggil train(False) untuk mematikan kecerunan, dan kesannya serupa dengan no_grad().
Jadi, kereta api biasanya dipanggil untuk menghidupkan kecerunan sebelum latihan. Selepas latihan, panggil eval untuk menutup kecerunan untuk memudahkan inferens pantas.
Selain mengira kebarangkalian kategori, perkara yang paling penting dalam perambatan ke hadapan ialah menyusun tensor dalam rangkaian ke dalam graf pengiraan dalam susunan perambatan ke hadapan. Tujuan Ia digunakan untuk mengira kecerunan setiap tensor semasa perambatan belakang.
Tensor dalam rangkaian saraf bukan sahaja digunakan untuk menyimpan data, tetapi juga untuk mengira dan menyimpan kecerunan.
Ambil operasi lilitan lapisan pertama sebagai contoh untuk melihat cara menjana graf pengiraan.
def conv2d(x: tensor.Tensor, kernel: tensor.Tensor, padding: int = 0, stride: int = 1): '''二维卷积函数 ''' N, _, _, _ = x.shape out_channels, _, kernel_size, _ = kernel.shape pad_x = __pad2d(x, padding) col = __im2col2d(pad_x, kernel_size, stride) out_h, out_w = col.shape[-2:] col = col.transpose(0, 4, 5, 1, 2, 3).reshape(N * out_h * out_w, -1) col_filter = kernel.reshape(out_channels, -1).T out = col @ col_filter return out.reshape(N, out_h, out_w, -1).transpose(0, 3, 1, 2)
x ialah imej input, dan tidak perlu merakam kecerunan. Kernel ialah berat kernel lilitan dan perlu mengira kecerunan.
Jadi, tensor baharu yang dijana oleh pad_x = __pad2d(x, padding) juga tidak mempunyai kecerunan, jadi ia tidak perlu ditambah pada graf pengiraan.
Tensor yang dijana oleh kernel.reshape(out_channels, -1) perlu mengira kecerunan dan juga perlu ditambah pada graf pengiraan.
Mari kita lihat proses penyambungan:
def reshape(self, *new_shape): return reshape(self, new_shape) class reshape(UnaryOperator): ''' 张量形状变换算子,在Tensor中进行重载 Parameters ---------- new_shape : tuple 变换后的形状,用法同NumPy ''' def __init__(self, x: Tensor, new_shape: tuple) -> None: self.new_shape = new_shape super().__init__(x) def forward(self, x: Tensor) return x.data.reshape(self.new_shape) def grad_fn(self, x: Tensor, grad: np.ndarray) return grad.reshape(x.shape)
Fungsi reshape akan mengembalikan objek kelas reshape Kelas reshape mewarisi kelas UnaryOperator dan dalam __init__. fungsi, Fungsi permulaan kelas induk dipanggil.
class UnaryOperator(Tensor): def __init__(self, x: Tensor) -> None: if not isinstance(x, Tensor): x = Tensor(x) self.device = x.device super().__init__( data=self.forward(x), device=x.device, # 这里 requires_grad 为 True requires_grad=is_grad_enable() and x.requires_grad, )
Kelas UnaryOperator mewarisi kelas Tensor, jadi objek bentuk semula juga merupakan tensor.
Dalam fungsi __init__ UnaryOperator, panggil fungsi pemulaan Tensor dan parameter_grad yang diperlukan adalah Benar, yang bermaksud bahawa kecerunan perlu dikira. Kod pengiraan
requires_grad ialah is_grad_enable() dan x.requires_grad is_grad_enable() telah ditetapkan kepada True dengan kereta api, dan x ialah kernel convolution, dan require_gradnya juga True.
class Tensor: def __init__( self, data: Any, dtype=None, device: Union[Device, int, str, None] = None, requires_grad: bool = False, ) -> None: if self.requires_grad: # 不需要求梯度的节点不出现在动态计算图中 Graph.add_node(self)
Akhir sekali, dalam kaedah permulaan kelas Tensor, panggil Graph.add_node(self) untuk menambah tensor semasa pada graf pengiraan.
Begitu juga, tensor baharu yang biasa dilihat di bawah menggunakan tensor yang memerlukan_grad=True akan diletakkan dalam graf pengiraan.
Selepas operasi lilitan, 6 nod akan ditambah pada graf pengiraan.
Selepas satu rambatan ke hadapan selesai, mulakan dari nod terakhir dalam graf pengiraan dan lakukan rambatan belakang dari belakang ke hadapan.
l = loss(y_hat, y) l.backward()
disebarkan lapisan demi lapisan melalui rangkaian hadapan dan akhirnya dihantar ke loss tensor l.
Mengambil l sebagai titik permulaan dan merambat dari hadapan ke belakang, kecerunan setiap nod dalam graf pengiraan boleh dikira.
Kod teras ke belakang adalah seperti berikut:
def backward(self, retain_graph: bool = False): for node in Graph.node_list[y_id::-1]: grad = node.grad for last in [l for l in node.last if l.requires_grad]: add_grad = node.grad_fn(last, grad) last.grad += add_grad
Graph.node_list[y_id::-1] mengisih graf pengiraan dalam susunan songsang.
node是前向传播时放入计算图中的每个tensor。
node.last 是生成当前tensor的直接父节点。
调用node.grad_fn计算梯度,并反向传给它的父节点。
grad_fn其实就是Tensor的求导公式,如:
class pow(BinaryOperator): ''' 幂运算算子,在Tensor类中进行重载 See also -------- add : 加法算子 ''' def grad_fn(self, node: Tensor, grad: np.ndarray) if node is self.last[0]: return (self.data * self.last[1].data / node.data) * grad
return后的代码其实就是幂函数求导公式。
假设y=x^2,x的导数为2x。
反向传播计算梯度后,便可以调用优化器,更新模型参数。
l.backward() optimizer.step()
本次训练我们用梯度下降SGD算法优化参数,更新过程如下:
def step(self): for i in range(len(self.params)): grad = self.params[i].grad + self.weight_decay * self.params[i].data self.v[i] *= self.momentum self.v[i] += self.lr * grad self.params[i].data -= self.v[i] if self.nesterov: self.params[i].data -= self.lr * grad
self.params是整个网络的权重,初始化SGD时传进去的。
step函数最核心的两行代码,self.v[i] += self.lr * grad 和 self.params[i].data -= self.v[i],用当前参数 - 学习速率 * 梯度更新当前参数。
这是机器学习的基础内容了,我们应该很熟悉了。
一次模型训练的完整过程大致就串完了,大家可以设置打印语句,或者通过DEBUG的方式跟踪每一行代码的执行过程,这样可以更了解模型的训练过程。
Atas ialah kandungan terperinci sukar dipercayai! Gunakan Numpy untuk membangunkan rangka kerja pembelajaran mendalam dan melihat ke dalam proses latihan rangkaian saraf. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!