简单神经网络搭建
前言
程序设计课期末作业要求实现一个「基于可穿戴传感器数据的人体活动识别」,具体是实现一个分类任务。
数据采集设备以每秒 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 激活函数。

并且定义对 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%