简单神经网络搭建

前言

程序设计课期末作业要求实现一个「基于可穿戴传感器数据的人体活动识别」,具体是实现一个分类任务。

数据采集设备以每秒 1 次的频率记录传感器信息,涵盖加速度(x、y、 z 轴)、陀螺仪角速度(x、y、z 轴)、温度、湿度、心率、皮肤电反应等共计 561 个数值型特征维度。每条数据均标注了活动类别标签,包括以下六类:

WALKING:行走,标记为 1

WALKING_UPSTAIRS:上楼梯,标记为 2

WALKING_DOWNSTAIRS:下楼梯,标记为 3

SITTING:坐着,标记为 4

STANDING:站着,标记为 5

LAYING:平躺,标记为 6

基本设计

以下代码依赖 C++ libtorch 库,使用 CUDA

数据处理

读入给定的训练集 X_test.txt 和对应的标签 y_test.txt,并且使用 from_blob 转换为张量类型 torch::tensor()

class CustomDataset : public torch::data::Dataset<CustomDataset> {
private:
    std::vector<torch::Tensor> features_;
    std::vector<torch::Tensor> labels_;

public:
    CustomDataset(const std::string& feature_file, const std::string& label_file) {
        std::ifstream ffile(feature_file);
        std::ifstream lfile(label_file);

        if (!ffile.is_open() || !lfile.is_open()) {
            throw std::runtime_error("无法打开特征或标签文件");
        }

        std::vector<float> buffer;
        std::string line;

        // 读取特征
        while (std::getline(ffile, line)) {
            std::istringstream ss(line);
            float val;
            while (ss >> val) {
                buffer.push_back(val);
            }
        }

        size_t total_samples = buffer.size() / 561;
        for (size_t i = 0; i < total_samples; ++i) {
            torch::Tensor x = torch::from_blob(buffer.data() + i * 561, {561}, torch::kFloat).clone();
            features_.push_back(x);
        }

        // 读取标签
        while (std::getline(lfile, line)) {
            int label = std::stoi(line) - 1; //label:1-6 (expected 0-5)
            labels_.push_back(torch::tensor(label, torch::kLong));
        }

        if (features_.size() != labels_.size()) {
            throw std::runtime_error("特征数与标签数不一致!");
        }
    }

};

定义模型

为了先让程序跑起来,考虑使用单层神经网络实现需求。

即只包含一个线性层,其中输入样本特征的大小 561,输出样本特征的大小 6。

定义:

  • 一个结构体,继承 torch::nn::Module

  • 线性层 torch::nn::Linear

  • 前向传播函数 torch::Tensor forward(torch::Tensor x)

使用宏 TORCH_MODULE(),使程序可以调用 torch::save(), torch::load()

struct NetImpl : torch::nn::Module {
    torch::nn::Linear fc{nullptr};
    NetImpl() {
        fc = register_module("fc", torch::nn::Linear(561, 6));
    }

    torch::Tensor forward(torch::Tensor x) {
        return fc(x);
    }
};

TORCH_MODULE(Net);

训练过程

此训练过程基于小批量随机梯度下降(SGD)优化算法。具体而言,每个训练迭代将处理一个包含 8 个样本的小批量数据。

在每个训练周期内: 1. 前向传播 2. 损失计算 3. 梯度清零 4. 反向传播 5. 参数更新

使用 torch::data::Dataset<CustomDataset>map 方法,应用转换 (transform)torch::data::transforms::Stack<>() 到数据集的每个单独样本上。这将让多个单独张量堆叠,形成一个批次张量。

const std::string feature_file = "/home/summer/CLionProjects/cppAssignment202505/X_train.txt";
const std::string label_file = "/home/summer/CLionProjects/cppAssignment202505/y_train.txt";
const size_t batch_size = 8;
const size_t num_epochs = 10;
const double learning_rate = 0.01;

auto dataset = CustomDataset(feature_file, label_file).map(torch::data::transforms::Stack<>());
auto data_loader = torch::data::make_data_loader(dataset, batch_size);

Net model = Net();
torch::optim::SGD optimizer(model->parameters(), learning_rate);

for (size_t epoch = 1; epoch <= num_epochs; ++epoch) {
    model->train();
    double total_loss = 0.0;
    size_t batch_idx = 0;

    for (auto& batch : *data_loader) {
        auto data = batch.data;
        auto targets = batch.target;

        auto output = model->forward(data);
        auto loss = torch::nn::functional::cross_entropy(output, targets);

        optimizer.zero_grad();
        loss.backward();
        optimizer.step();
        total_loss += loss.template item<double>();
        ++batch_idx;
    }

    std::cout << "Epoch [" << epoch << "/" << num_epochs << "] Avg Loss: "
              << (total_loss / batch_idx) << std::endl;
}

训练完成后,保存模型

torch::save(model, "/home/summer/CLionProjects/cppAssignment202505/model.pt");

数据预测

为了评估模型的泛化能力,我们按照与训练数据相似的方法,读取并处理测试集

const std::string model_path = "/home/summer/CLionProjects/cppAssignment202505/model.pt";
const std::string feature_file = "/home/summer/CLionProjects/cppAssignment202505/X_test.txt";
const std::string label_file = "/home/summer/CLionProjects/cppAssignment202505/y_test.txt";
const size_t batch_size = 8;

Net model= Net();
torch::load(model, model_path);
model->eval();

auto dataset = CustomDataset(feature_file, label_file).map(torch::data::transforms::Stack<>());
auto data_loader = torch::data::make_data_loader(dataset, batch_size);

size_t correct = 0;
size_t total = 0;

for (auto& batch : *data_loader) {
    auto data = batch.data;
    auto targets = batch.target;

    auto output = model->forward(data);
    auto pred = output.argmax(1);

    correct += pred.eq(targets).sum().template item<int64_t>();
    total += targets.size(0);
}

double accuracy = static_cast<double>(correct) / total * 100.0;
std::cout << "Test Accuracy: " << accuracy << "%" << std::endl;

使用上述神经网络得到的结果是:

Test Accuracy: 94.1636%

模型优化

采用残差网络(ResNet)优化模型

残差块

采用两层卷积层,两层标准化层以及 ReLU 激活函数。

Residual Block

并且定义对 identity 的下采样处理 downsample,使用 torch::nn::Sequential 可以将多个模块堆叠。

ResidualBlock1DImpl(int64_t in_channels, int64_t out_channels, int64_t stride = 1) {
    conv1 = register_module("conv1", torch::nn::Conv1d(torch::nn::Conv1dOptions(in_channels, out_channels, 3).stride(stride).padding(1).bias(false)));
    bn1 = register_module("bn1", torch::nn::BatchNorm1d(out_channels));
    conv2 = register_module("conv2", torch::nn::Conv1d(torch::nn::Conv1dOptions(out_channels, out_channels, 3).stride(1).padding(1).bias(false)));
    bn2 = register_module("bn2", torch::nn::BatchNorm1d(out_channels));

    if (stride != 1 || in_channels != out_channels) {
        downsample = register_module("downsample", torch::nn::Sequential(
            torch::nn::Conv1d(torch::nn::Conv1dOptions(in_channels, out_channels, 1).stride(stride).bias(false)),
            torch::nn::BatchNorm1d(out_channels)
        ));
    }
}

定义前向传播的两条路径:

torch::Tensor forward(torch::Tensor x) {
    auto identity = x.clone();
    x = torch::relu(bn1(conv1(x)));
    x = bn2(conv2(x));

    if (!downsample->is_empty()) {
        identity = downsample->forward(identity);
    }

    x += identity;
    return torch::relu(x);
}

残差网络

我们的网络架构基于 ResNet 的原理,并采用以下具体结构:

输入 -> 卷积层 -> 标准化层 -> 激活层 -> 残差块 -> 平均池化 -> 全连接层 -> 输出
struct ResNet1DImpl : torch::nn::Module {
    torch::nn::Conv1d conv{nullptr};
    torch::nn::BatchNorm1d bn{nullptr};
    torch::nn::Sequential layer1, layer2, layer3;
    torch::nn::Linear fc{nullptr};

    ResNet1DImpl() {
        conv = register_module("conv", torch::nn::Conv1d(torch::nn::Conv1dOptions(1, 64, 7).stride(2).padding(3).bias(false)));
        bn = register_module("bn", torch::nn::BatchNorm1d(64));

        layer1 = register_module("layer1", _make_layer(64, 64, 2, 1));
        layer2 = register_module("layer2", _make_layer(64, 64, 2, 2));
        layer3 = register_module("layer3", _make_layer(64, 128, 2, 2));

        fc = register_module("fc", torch::nn::Linear(128, 6));
    }

    torch::nn::Sequential _make_layer(int64_t in_channels, int64_t out_channels, int blocks, int stride) {
        torch::nn::Sequential layers;
        layers->push_back(ResidualBlock1D(in_channels, out_channels, stride));
        for (int i = 1; i < blocks; ++i) {
            layers->push_back(ResidualBlock1D(out_channels, out_channels));
        }
        return layers;
    }

    torch::Tensor forward(torch::Tensor x) {
        x = x.unsqueeze(1); // (batch, 1, 561)
        x = torch::relu(bn(conv(x)));
        x = layer1->forward(x);
        x = layer2->forward(x);
        x = layer3->forward(x);

        x = torch::adaptive_avg_pool1d(x, 1);
        x = x.view({x.size(0), -1});
        x = fc(x);
        return x;
    }
};
TORCH_MODULE(ResNet1D);

性能优化

为了加快训练速度,可以将训练过程转移到 GPU 上运行

torch::manual_seed(42);

torch::Device device(torch::kCPU);
if (torch::cuda::is_available()) {
    std::cout << "CUDA is available! Training on GPU." << std::endl;
    device = torch::Device(torch::kCUDA);
}

const std::string train_feature_file = "/home/summer/CLionProjects/cppAssignment202505/X_train.txt";
const std::string train_label_file = "/home/summer/CLionProjects/cppAssignment202505/y_train.txt";
const std::string test_feature_file = "/home/summer/CLionProjects/cppAssignment202505/X_test.txt";
const std::string test_label_file = "/home/summer/CLionProjects/cppAssignment202505/y_test.txt";

const size_t batch_size = 8;
const size_t num_epochs = 15;
const double learning_rate = 0.001;

auto train_dataset = CustomDataset(train_feature_file, train_label_file).map(torch::data::transforms::Stack<>());
auto train_loader = torch::data::make_data_loader(train_dataset, batch_size);

auto test_dataset = CustomDataset(test_feature_file, test_label_file).map(torch::data::transforms::Stack<>());
auto test_loader = torch::data::make_data_loader(test_dataset, batch_size);

ResNet1D model = ResNet1D();
model->to(device);

torch::optim::SGD optimizer(model->parameters(), torch::optim::SGDOptions(learning_rate).momentum(0.9));

for (size_t epoch = 1; epoch <= num_epochs; ++epoch) {
    model->train();
    double total_loss = 0.0;
    size_t batch_idx = 0;

    for (auto& batch : *train_loader) {
        auto data = batch.data.to(device);
        auto targets = batch.target.to(device);

        optimizer.zero_grad();
        auto output = model->forward(data);
        auto loss = torch::nn::functional::cross_entropy(output, targets);
        loss.backward();
        optimizer.step();

        total_loss += loss.template item<double>();
        batch_idx++;
    }

    std::cout << "Epoch [" << epoch << "/" << num_epochs << "] Avg Loss: " << total_loss / batch_idx << std::endl;
}

预测部分如下:

const std::string model_path = "/home/summer/CLionProjects/cppAssignment202505/model.pt";
const std::string feature_file = "/home/summer/CLionProjects/cppAssignment202505/X_test.txt";
const std::string label_file = "/home/summer/CLionProjects/cppAssignment202505/y_test.txt";
const size_t batch_size = 8;

torch::Device device(torch::kCPU);
if (torch::cuda::is_available()) {
    std::cout << "CUDA is available! Training on GPU." << std::endl;
    device = torch::Device(torch::kCUDA);
}

ResNet1D model = ResNet1D();

try {
    torch::load(model, model_path);
    std::cout << "Model loaded successfully from: " << model_path << std::endl;
} catch (const c10::Error& e) {
    std::cerr << "Error loading model: " << e.what() << std::endl;
    return -1;
}

model->to(device);
model->eval();

auto dataset = CustomDataset(feature_file, label_file).map(torch::data::transforms::Stack<>());
auto data_loader = torch::data::make_data_loader(dataset, batch_size);

size_t correct = 0;
size_t total = 0;

torch::NoGradGuard no_grad;

for (auto& batch : *data_loader) {
    auto data = batch.data;
    auto targets = batch.target;

    data = data.to(device);
    targets = targets.to(device);

    auto output = model->forward(data);

    auto pred = output.argmax(1);


    correct += pred.eq(targets).sum().template item<int64_t>();
    total += targets.size(0);
}


double accuracy = static_cast<double>(correct) / total * 100.0;
std::cout << "Test Accuracy: " << accuracy << "%" << std::endl;

return 0;

改进后的模型得到的结果是:

Test Accuracy: 96.4031%