51、STL与机器学习:特征向量、数据预处理、模型参数存储

说实话,很多人觉得STL跟机器学习八竿子打不着。机器学习嘛,那得用Python、用NumPy、用TensorFlow。但我在做嵌入式端模型部署的时候,发现一个尴尬的问题——Python那一套在资源受限的环境下根本跑不动。这时候,C++和STL就成了我的救命稻草。

今天我们就聊聊,怎么用STL来搞定机器学习中的三个核心环节:特征向量的表示、数据预处理、以及模型参数的存储与加载。嗯,这里要注意,我们不是要造一个深度学习框架,而是让你理解STL在这些场景下能发挥什么作用。

特征向量:用vector就够了?

特征向量说白了就是一串数值。比如一张图片的像素值、一段文本的词频统计、一个用户的年龄身高体重。在C++里,最直观的表示方式就是std::vector

#include <vector>
#include <cstdint>

// 一个简单的特征向量
std::vector<float> features = {0.5f, 1.2f, 3.8f, 2.1f};

但我在项目中遇到过一个问题:当特征维度特别高时(比如几万维),频繁的拷贝操作会让性能崩掉。你想想看,每次传参都复制几万个float,那速度能快吗?

我的习惯是:

  • 只读访问:用const std::vector<float>&传参
  • 需要修改:用std::vector<float>&传参
  • 避免拷贝:用std::move转移所有权

核心原则:特征向量在机器学习中通常是只读的,或者只做一次预处理。不要反复拷贝,否则你的训练还没开始,时间全浪费在内存搬运上了。

如果特征维度是固定的,我建议用std::array代替std::vector。为什么?因为std::array是栈上分配,没有堆内存开销,访问速度更快。

#include <array>

// 固定维度128的特征向量
using FeatureVector = std::array<float, 128>;

FeatureVector fv = {};
fv[0] = 0.5f;

但要注意,std::array的维度必须在编译期确定。如果你的特征维度是运行时才知道的(比如从配置文件读取),那就只能用std::vector了。

数据预处理:STL算法来帮忙

数据预处理是机器学习中最枯燥但最重要的一步。原始数据往往有缺失值、量纲不统一、噪声大等问题。用STL算法可以优雅地解决这些问题。

归一化

归一化就是把不同量纲的特征缩放到同一个范围。最常见的做法是Min-Max归一化:

#include <algorithm>
#include <vector>
#include <numeric>

void min_max_normalize(std::vector<float>& data) {
    if (data.empty()) return;

    auto [min_it, max_it] = std::minmax_element(data.begin(), data.end());
    float min_val = *min_it;
    float max_val = *max_it;
    float range = max_val - min_val;

    if (range == 0.0f) return;  // 防止除零

    std::transform(data.begin(), data.end(), data.begin(),
        [min_val, range](float x) {
            return (x - min_val) / range;
        });
}

这里用了std::minmax_element一次性找到最小值和最大值,比分别调用std::min_elementstd::max_element效率更高。我曾经在优化一个推荐系统的特征预处理时,就靠这个函数省下了不少时间。

标准化

标准化(Z-score)是另一种常见操作,它让数据变成均值为0、标准差为1的分布:

void z_score_normalize(std::vector<float>& data) {
    if (data.empty()) return;

    float mean = std::accumulate(data.begin(), data.end(), 0.0f) / data.size();

    float variance = 0.0f;
    std::for_each(data.begin(), data.end(), [&](float x) {
        variance += (x - mean) * (x - mean);
    });
    variance /= data.size();

    float stddev = std::sqrt(variance);
    if (stddev == 0.0f) return;

    std::transform(data.begin(), data.end(), data.begin(),
        [mean, stddev](float x) {
            return (x - mean) / stddev;
        });
}

小技巧:如果你需要同时处理多个特征列,可以用std::vector<std::vector<float>>来表示一个特征矩阵。但要注意内存布局,按列存储比按行存储对缓存更友好。

缺失值处理

原始数据里经常有缺失值,比如用NaN或-1表示。我的做法是先过滤掉无效数据,再用均值或中位数填充:

#include <cmath>
#include <iterator>

void fill_missing_with_mean(std::vector<float>& data) {
    // 先计算有效值的均值
    float sum = 0.0f;
    int count = 0;
    for (float x : data) {
        if (!std::isnan(x)) {
            sum += x;
            ++count;
        }
    }
    float mean = (count > 0) ? (sum / count) : 0.0f;

    // 填充缺失值
    std::replace_if(data.begin(), data.end(),
        [](float x) { return std::isnan(x); },
        mean);
}

这里用了std::replace_if,配合lambda表达式,一行就搞定了替换逻辑。STL的算法组合起来用,威力很大。

模型参数存储:序列化与反序列化

模型训练完了,参数得存下来吧?下次推理的时候再加载。这就是序列化和反序列化。STL没有直接提供序列化功能,但我们可以用<fstream>配合std::vector来实现。

二进制存储

我最推荐的方式是二进制存储,速度快、体积小:

#include <fstream>
#include <vector>

// 保存模型参数
void save_model_params(const std::vector<float>& params, const std::string& path) {
    std::ofstream ofs(path, std::ios::binary);
    if (!ofs) {
        throw std::runtime_error("无法打开文件: " + path);
    }

    // 先写入参数数量
    size_t size = params.size();
    ofs.write(reinterpret_cast<const char*>(&size), sizeof(size));

    // 再写入参数数据
    ofs.write(reinterpret_cast<const char*>(params.data()), size * sizeof(float));
}

// 加载模型参数
std::vector<float> load_model_params(const std::string& path) {
    std::ifstream ifs(path, std::ios::binary);
    if (!ifs) {
        throw std::runtime_error("无法打开文件: " + path);
    }

    size_t size = 0;
    ifs.read(reinterpret_cast<char*>(&size), sizeof(size));

    std::vector<float> params(size);
    ifs.read(reinterpret_cast<char*>(params.data()), size * sizeof(float));

    return params;
}

注意:二进制文件在不同平台间可能不兼容(大小端问题)。如果你的模型要在不同架构的机器上部署,建议用文本格式或者加上字节序标记。

文本存储

有时候为了调试方便,我会用文本格式存储参数。比如用JSON或简单的CSV格式。STL的std::stringstream可以帮上忙:

#include <sstream>
#include <string>

std::string params_to_string(const std::vector<float>& params) {
    std::ostringstream oss;
    for (size_t i = 0; i < params.size(); ++i) {
        if (i > 0) oss << ",";
        oss << params[i];
    }
    return oss.str();
}

std::vector<float> string_to_params(const std::string& str) {
    std::vector<float> params;
    std::istringstream iss(str);
    std::string token;

    while (std::getline(iss, token, ',')) {
        params.push_back(std::stof(token));
    }
    return params;
}

文本格式的好处是可读性强,坏处是体积大、解析慢。我个人习惯是:开发调试用文本,生产部署用二进制。

知识体系总览

下面这张图总结了本章的核心内容,你可以看到STL在机器学习三个环节中的具体应用:

STL与机器学习核心环节 特征向量 数据预处理 模型参数存储 std::vector / std::array const引用传参 / std::move 固定维度 vs 动态维度 std::minmax_element 归一化 std::accumulate 标准化 std::replace_if 缺失值填充 二进制存储 (fstream) 文本存储 (stringstream) 序列化 / 反序列化 核心思想:用STL容器管理数据,用STL算法处理数据,用STL IO持久化数据 避免重复造轮子,STL已经提供了足够强大的基础工具

避坑指南

最后,分享几个我踩过的坑:

  • 我曾经在保存模型参数时忘了考虑字节对齐,结果在ARM平台上加载x86上保存的参数,数值全乱了。解决方案是逐元素序列化,或者用#pragma pack控制对齐。
  • 我曾经std::vector<bool>来存储二值特征,结果发现它的底层实现是位压缩,访问速度比普通vector慢很多。后来我换成了std::vector<char>或者std::bitset
  • 我曾经在数据预处理时直接修改原始数据,导致后续调试时找不到原始值。现在我总是先拷贝一份再处理,或者用std::transform输出到新容器。

STL在机器学习中的应用远不止这些。你想想看,std::unordered_map可以用来做特征哈希,std::priority_queue可以用来做Top-K筛选,std::sort可以用来排序特征重要性。STL就像一把瑞士军刀,关键看你怎么用。

我的建议:不要一上来就想用复杂的第三方库。先用STL把基础功能跑通,性能不够再考虑优化。很多时候,STL的默认实现已经足够快了。

好了,这一章就聊到这里。记住,STL不是机器学习的对立面,而是你工具箱里最趁手的那把锤子。用好了,事半功倍。

公众号:蓝海资料掘金营,微信deep3321