在上一篇文章《機器學習:神經網絡構建(上)》中討論了線性層、激活函數以及損失函數層的構建方式,本節(jié)中將進一步討論網絡構建方式,并完整的搭建一個簡單的分類器網絡。
在上一篇文章《 機器學習:神經網絡構建(上) 》中討論了線性層、激活函數以及損失函數層的構建方式,本節(jié)中將進一步討論網絡構建方式,并完整的搭建一個簡單的分類器網絡。
在設計神經網絡時,其基本結構是由一層層的神經元組成的,這些層可以是輸入層、隱藏層和輸出層。為了實現這一結構,通常會使用向量(vector)容器來存儲這些層,因為層的數量是可變的,可能根據具體任務的需求而變化。
即使在網絡已經進行了預訓練并具有一定的參數的情況下,對于特定的任務,通常還是需要進行模型微調。這是因為不同的任務可能有不同的數據分布和要求,因此訓練是構建高性能神經網絡模型的重要步驟。
在訓練過程中,有三個關鍵組件:
損失函數 :神經網絡的學習目標,通過最小化損失函數來優(yōu)化模型參數。選擇合適的損失函數對于確保模型能夠學習到有效的特征表示至關重要。
優(yōu)化器 :優(yōu)化器負責調整模型的參數以最小化損失函數。除了基本的參數更新功能外,優(yōu)化器還可以提供更高級的功能,如學習率調整和參數凍結,這些功能有助于提高訓練效率和模型性能。
數據集管理器 :負責在訓練過程中有效地管理和提供數據,包括數據的加載、預處理和批處理,以確保數據被充分利用。
對于網絡的外部接口(公有方法),主要有以下幾類:
以下是代碼示例:
class Network {
private:
vector> layers;
shared_ptr lossFunction;
shared_ptr optimizer;
shared_ptr datasetManager;
public:
void addLayer(shared_ptr layer);
void setLossFunction(shared_ptr lossFunc);
void setOptimizer(shared_ptr opt);
void setDatasetManager(shared_ptr manager);
MatrixXd forward(const MatrixXd& input);
void backward(const MatrixXd& outputGrad);
double train(size_t epochs, size_t batchSize);
};
使用shared_ptr的好處:
存儲方式vector
網絡的訓練函數通常包含兩個輸入參數,訓練的集數和批尺寸:
集數
epochs
:指訓練集被完整的迭代的次數。在每一個epoch中,網絡會使用訓練集中的所有樣本進行參數更新。
批尺寸
batchSize
:指在一次迭代中用于更新模型參數的樣本數量。在每次迭代中,模型會計算這些樣本的總梯度,并據此調整模型的參數。
因此,網絡的訓練函數由兩層循環(huán)結構組成,外層循環(huán)結構表示完整迭代的次數,直至完成所有迭代時停止。內層循環(huán)表示訓練集中樣本被網絡調取的進度,直至訓練集中的所有數據被調用時停止。
網絡的訓練過程是由多次的參數迭代(更新)完成的。而參數的的迭代是以批(Batch)為單位的。具體來說,一次迭代包含如下步驟:
代碼設計如下:
double Network::train(size_t epochs, size_t batchSize) {
double totalLoss = 0.0;
size_t sampleCount = datasetManager->getTrainSampleCount();
for (size_t epoch = 0; epoch < epochs; ++epoch) {
datasetManager->shuffleTrainSet();
totalLoss = 0.0;
for (size_t i = 0; i < sampleCount; i += batchSize) {
// 獲取一個小批量樣本
auto batch = datasetManager->getTrainBatch(batchSize, i / batchSize);
MatrixXd batchInput = batch.first;
MatrixXd batchLabel = batch.second;
// 前向傳播
MatrixXd predicted = forward(batchInput);
double loss = lossFunction->computeLoss(predicted, batchLabel);
// 反向傳播
MatrixXd outputGrad = lossFunction->computeGradient(predicted, batchLabel);
backward(outputGrad);
// 參數更新
optimizer->update(layers);
// 累計損失
totalLoss += loss;
}
totalLoss /= datasetManager->getTrainSampleCount();
// 輸出每個epoch的損失等信息
std::cout << "Epoch " << epoch << ", totalLoss = " << totalLoss << "\n";
}
return totalLoss / (epochs * (sampleCount / batchSize)); // 返回平均損失(簡化示例)
}
下面的代碼給出了網絡的其它公有方法的代碼實現:
void Network::addLayer(std::shared_ptr layer) {
layers.push_back(layer);
}
void Network::setLossFunction(std::shared_ptr lossFunc) {
lossFunction = lossFunc;
}
void Network::setOptimizer(std::shared_ptr opt) {
optimizer = opt;
}
void Network::setDatasetManager(std::shared_ptr manager) {
datasetManager = manager;
}
MatrixXd Network::forward(const MatrixXd& input) {
MatrixXd currentInput = input;
for (const auto& layer : layers) {
currentInput = layer->forward(currentInput);
}
return currentInput;
}
void Network::backward(const MatrixXd& outputGrad) {
MatrixXd currentGrad = outputGrad;
for (auto it = layers.rbegin(); it != layers.rend(); ++it) {
currentGrad = (*it)->backward(currentGrad);
}
}
forward
方法除了作為訓練時的步驟之一,還經常用于網絡推理(預測),因此聲明為公有方法
backward
方法只在訓練時使用,在正常的使用用途中,不會被外部調用,因此,其可以聲明為私有方法。
數據集管理器本質目的是提高網絡對數據的利用率,其主要職能有:
class DatasetManager {
private:
MatrixXd input;
MatrixXd label;
std::vector trainIndices;
std::vector valIndices;
std::vector testIndices;
public:
// 設置數據集的方法
void setDataset(const MatrixXd& inputData, const MatrixXd& labelData);
// 劃分數據集為訓練集、驗證集和測試集
void splitDataset(double trainRatio = 0.8, double valRatio = 0.1, double testRatio = 0.1);
// 獲取訓練集、驗證集和測試集的小批量數據
std::pair getBatch(std::vector& indices, size_t batchSize, size_t offset = 0);
// 隨機打亂訓練集
void shuffleTrainSet();
// 獲取批量數據
std::pair getTrainBatch(size_t batchSize, size_t offset = 0);
std::pair getValidationBatch(size_t batchSize, size_t offset = 0);
std::pair getTestBatch(size_t batchSize, size_t offset = 0);
// 獲取樣本數量的方法
size_t getSampleCount() const;
size_t getTrainSampleCount() const;
size_t getValidationSampleCount() const;
size_t getTestSampleCount() const;
};
數據集初始化分為三步:數據集設置、數據集劃分、數據集打亂。
// 設置數據集
void ML::DatasetManager::setDataset(const MatrixXd& inputData, const MatrixXd& labelData) {
input = inputData;
label = labelData;
trainIndices.resize(input.rows());
std::iota(trainIndices.begin(), trainIndices.end(), 0);
valIndices.clear();
testIndices.clear();
}
// 打亂訓練集
void ML::DatasetManager::shuffleTrainSet() {
std::shuffle(trainIndices.begin(), trainIndices.end(), std::mt19937{ std::random_device{}() });
}
// 劃分數據集為訓練集、驗證集和測試集
void ML::DatasetManager::splitDataset(double trainRatio, double valRatio, double testRatio) {
size_t totalSamples = input.rows();
size_t trainSize = static_cast(totalSamples * trainRatio);
size_t valSize = static_cast(totalSamples * valRatio);
size_t testSize = totalSamples - trainSize - valSize;
shuffleTrainSet();
valIndices.assign(trainIndices.begin() + trainSize, trainIndices.begin() + trainSize + valSize);
testIndices.assign(trainIndices.begin() + trainSize + valSize, trainIndices.end());
trainIndices.resize(trainSize);
}
對于打亂操作較頻繁的場景,打亂索引是更為高效的操作;而對于不經常打亂的場景,直接在數據集上打亂更為高效。本例中僅給出打亂索引的代碼示例。
在獲取數據時,首先明確所需數據集的類型(訓練集或驗證集)。然后,根據預設的批次大。˙atchsize),從索引列表中提取相應數量的索引,并將這些索引對應的數據存儲到臨時矩陣中。最后,導出數據,完成讀取操作。
// 獲取訓練集、驗證集和測試集的小批量數據
std::pair ML::DatasetManager::getBatch(std::vector& indices, size_t batchSize, size_t offset) {
size_t start = offset * batchSize;
size_t end = std::min(start + batchSize, indices.size());
MatrixXd batchInput = MatrixXd::Zero(end - start, input.cols());
MatrixXd batchLabel = MatrixXd::Zero(end - start, label.cols());
for (size_t i = start; i < end; ++i) {
batchInput.row(i - start) = input.row(indices[i]);
batchLabel.row(i - start) = label.row(indices[i]);
}
return std::make_pair(batchInput, batchLabel);
}
// 獲取訓練集的批量數據
std::pair ML::DatasetManager::getTrainBatch(size_t batchSize, size_t offset) {
return getBatch(trainIndices, batchSize, offset);
}
// 獲取驗證集的批量數據
std::pair ML::DatasetManager::getValidationBatch(size_t batchSize, size_t offset) {
return getBatch(valIndices, batchSize, offset);
}
// 獲取測試集的批量數據
std::pair ML::DatasetManager::getTestBatch(size_t batchSize, size_t offset) {
return getBatch(testIndices, batchSize, offset);
}
為便于代碼開發(fā),需要為數據集管理器設計外部接口,以便于外部可以獲取各個數據集的尺寸。
size_t ML::DatasetManager::getSampleCount() const {
return input.rows();
}
size_t ML::DatasetManager::getTrainSampleCount() const {
return trainIndices.size();
}
size_t ML::DatasetManager::getValidationSampleCount() const {
return valIndices.size();
}
size_t ML::DatasetManager::getTestSampleCount() const {
return testIndices.size();
}
隨機梯度下降是一種優(yōu)化算法,用于最小化損失函數以訓練模型參數。與批量梯度下降(Batch Gradient Descent)不同,SGD在每次更新參數時只使用一個樣本(或一個小批量的樣本),而不是整個訓練集。這使得SGD在計算上更高效,且能夠更快地收斂,尤其是在處理大規(guī)模數據時。以下為隨機梯度下降的代碼示例:
class Optimizer {
public:
virtual void update(std::vector>& layers) = 0;
virtual ~Optimizer() {}
};
class SGDOptimizer : public Optimizer {
private:
double learningRate;
public:
SGDOptimizer(double learningRate) : learningRate(learningRate) {}
void update(std::vector>& layers) override;
};
void SGDOptimizer::update(std::vector>& layers) {
for (auto& layer : layers) {
layer->update(learningRate);
}
}
如果你希望測試這些代碼,首先可以從本篇文章,以及
上一篇文章
中復制代碼,并參考下述圖片構建你的解決方案。
如果你有遇到問題,歡迎聯系作者!
下述代碼為線性回歸的測試樣例:
namespace LNR{
// linear_regression
void gen(MatrixXd& X, MatrixXd& y);
void test();
}
void LNR::gen(MatrixXd& X, MatrixXd& y) {
MatrixXd w(X.cols(), 1);
X.setRandom();
w.setRandom();
X.rowwise() -= X.colwise().mean();
X.array().rowwise() /= X.array().colwise().norm();
y = X * w;
}
void LNR::test() {
std::cout << std::fixed << std::setprecision(2);
size_t input_dim = 10;
size_t sample_num = 2000;
MatrixXd X(sample_num, input_dim);
MatrixXd y(sample_num, 1);
gen(X, y);
ML::DatasetManager dataset;
dataset.setDataset(X, y);
ML::Network net;
net.addLayer(std::make_shared(input_dim, 1));
net.setLossFunction(std::make_shared());
net.setOptimizer(std::make_shared(0.25));
net.setDatasetManager(std::make_shared(dataset));
size_t epochs = 600;
size_t batch_size = 50;
net.train(epochs, batch_size);
MatrixXd error(sample_num, 1);
error = net.forward(X) - y;
std::cout << "error=\n" << error << "\n";
}
詳細解釋
gen
函數:用以生成測試數據。
輸出展示
完成訓練后,網絡預測值與真實值的誤差如下圖;容易發(fā)現,網絡具有較好的預測精度。
下述代碼為邏輯回歸的測試樣例:
namespace LC {
// Linear classification
void gen(MatrixXd& X, MatrixXd& y);
void test();
}
void LC::gen(MatrixXd& X, MatrixXd& y) {
MatrixXd w(X.cols(), 1);
X.setRandom();
w.setRandom();
X.rowwise() -= X.colwise().mean();
X.array().rowwise() /= X.array().colwise().norm();
y = X * w;
y = y.unaryExpr([](double x) { return x > 0.0 ? 1.0 : 0.0; });
}
void LC::test() {
std::cout << std::fixed << std::setprecision(3);
size_t input_dim = 10;
size_t sample_num = 2000;
MatrixXd X(sample_num, input_dim);
MatrixXd y(sample_num, 1);
gen(X, y);
ML::DatasetManager dataset;
dataset.setDataset(X, y);
ML::Network net;
net.addLayer(std::make_shared(input_dim, 1));
net.addLayer(std::make_shared());
net.setLossFunction(std::make_shared());
net.setOptimizer(std::make_shared(0.05));
net.setDatasetManager(std::make_shared(dataset));
size_t epochs = 200;
size_t batch_size = 25;
net.train(epochs, batch_size);
MatrixXd predict(sample_num, 1);
predict = net.forward(X);
predict = predict.unaryExpr([](double x) { return x > 0.5 ? 1.0 : 0.0; });
MatrixXd error(sample_num, 1);
error = y - predict;
error = error.unaryExpr([](double x) {return (x < 0.01 && x>-0.01) ? 1.0 : 0.0; });
std::cout << "正確率=\n" << error.sum() / sample_num << "\n";
}
詳細解釋
gen
函數:用以生成測試數據。
輸出展示
下圖反映了網絡預測過程中的損失變化,可以看到損失逐漸下降的趨勢。
完成訓練后,輸出網絡的預測結果的正確率?梢园l(fā)現,網絡具有較好的預測精度。