在预先存在的数据中对数据进行编码的最佳算法

Optimal algorithm for encoding data within pre-existing data

提问人:Dillon Davis 提问时间:2/6/2023 更新时间:2/11/2023 访问量:145

问:

假设我们有一些现有的随机数据,均匀分布,已经写入了某种介质。还可以说,比特的写入是一种破坏性的动作,而对于比特来说,它是非破坏性的。这可能类似于在穿孔卡上打孔或在电路中烧掉保险丝以硬编码 .在进行初始写入之后,假设我们现在希望将其他数据写入该介质,并使用一些特殊的编码,使我们能够在未来检索这些新数据,而无需对已经破坏性写入介质的预先存在的数据有特殊了解。预先存在的数据本身不需要保持可检索性,只需要保持新数据的可检索性。101

还要假设新数据本身是随机的,或者至少已经被压缩为有效的随机数据。

显然,我们不能指望超过原始存储容量的~50%,因为只有大约一半是可写的。问题在于试图将效率尽可能接近这个极限。

我有两个相当简单的编码,效率看似合理,但它们似乎不是最佳的。为了便于解释,我将假设介质是带有孔(表示 a)或间隙(表示 a)的纸带,每隔一段时间就会出现。10

编码 A

在磁带上以偶数偏移量对 a 位进行编码,在奇数偏移处对 a 位进行编码。1gap0gap

Offset:    0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19
Tape:      G  G  H  H  H  H  H  G  H  H  H  G  H  H  H  H  G  H  H  H
           |  |                 |           |              |
New Data:  1  0                 0           0              1

编码 B

用 a 后跟 another 对位进行编码,用 a 后跟 a 对 .1gapgap0gaphole

Offset: 0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19
Tape:   H  G  G  H  H  H  H  G  H  H  H  G  G  H  H  H  G  H  G  H
            \/                \/          \/             \/    \/
New Data:   1                 0           1              0     0

平均而言,这两种方法都可以对原始存储容量的 25% 的数据进行编码。这似乎很合理,至少对我来说是这样——一开始只有一半的磁带是 s,我们写的任何 s 都会丢失信息(因为我们在以后尝试解码时无法知道它们是否预先存在),所以四分之一 - 25%。乍一看似乎是最佳的。gaphole

然而,困扰我的是,事实似乎并非如此。我有一个稍微复杂的编码,它始终打破这个阈值。

编码 C

从本质上讲,我们将位编码为奇数长度的 s 游程,将比特编码为偶数长度的 s 游程。所以会代表 while 或 would 代表 .1hole0holeHHG0HGHHHG1

然而,这本身并不足以跨越 25% 的阈值,因此我们添加了一个优化。当我们在 s 的运行和终止之后看到两个连续的 s 时,我们将它们视为 .这背后的逻辑是,如果我们想对 a 进行编码,我们可以直接打出两个间隙中的第一个,产生 ,编码 a 。由于我们没有这样做,我们可以假设我们需要对gapholegapgaps01HG10

通过这种优化,编码 C 达到了稳定的 26.666 +/- 0.005 存储效率,始终高于理论值 25%。我还有一些非常棘手的窥视孔优化,我可以将其附加到这种编码上,我怀疑这会将其效率提高到 28-30%,但感觉我已经想多了。

编码 D?

这并不是真正的编码,而是我最后一个想法,它可以改进上述任何编码。考虑一种编码,以及一些确定性+可逆的转换,它以某种任意方式改变数据。假设我们在要编码的数据中预置一点,然后对其进行编码。假设我们也改变了我们的数据,预置了一点,然后对其进行编码。然后,我们可以比较两者,看看哪个碰巧编码了更多的数据(效率更高),然后选择那个。总体上,改进会很小,取决于统计变化,我们确实牺牲了一点作为指标,但只要数据足够大,平均节省的比特就会超过损失的单个比特。这可以概括 - 将数据划分为几个分区,并在 2 个或更多编码之间进行选择,以随机最适合的编码为准,但这不是重点。总的来说,改进会很小,但它仍然应该是一个直接的改进 - 表明基本编码不是最佳的。E(x)T(x)0E('0' + DATA)1E('1' + T(DATA))E(x)


回顾一下 - 我正在寻找最有效(在一般情况下)无损编码,用于将数据写入已经使用纯随机数据半破坏性(仅 1)写入的介质。我真的相信最佳解决方案的效率在 30-50% 之间,但我正在苦苦挣扎。我希望有人可以分享什么是最佳编码,或者阐明围绕这个主题的相关文献/理论。

旁注:在试图更好地理解这个问题的过程中,我试图创建一个效率较低的算法,将最坏情况下的编码效率限制在 0% 以上的任何地方,但失败了。似乎无论我尝试哪种编码,即使一半的存储保证是可写的,在一个天文数字上不太可能发生的事件:预先存在数据的排序可以确保我们甚至无法编码新数据的一点。这并不是实际问题陈述的真正问题,因为我关心的是平均案例效率,但这令人不安。

算法 优化 编码 语言不可知信息

评论

1赞 Damien 2/6/2023
香农理论(熵)可以给出一个上限。重要的一点:在解码第二个数据时,我们是否可以访问第一个数据,即 1 的位置?
0赞 Dillon Davis 2/6/2023
@Damien 我会看看香农理论,谢谢你的建议
0赞 Dillon Davis 2/6/2023
@Damien 在解码第二个数据时,我们无法访问第一个数据
0赞 trincot 2/6/2023
测试方法 A,我得到的结果为 28.57% ± 0.05%,这似乎明显高于您的结果。你是怎么测试的?
0赞 Dillon Davis 2/7/2023
@trincot 显然,由于没有足够的数据——通过一些粗略的数学计算,我得到了 25% 的预期结果,然后对每个测试进行了(太)小的测试,结果为 25%。

答:

2赞 David Eisenstat 2/6/2023 #1

我怀疑预期容量接近限制的 50%,因为 位数 n → ∞。

我想到的编码算法使用线性代数 有限域 F2.提前选择 ε > 0 和随机 维数为 (1 − ε) n/2 × n 的矩阵 A。解码向量 x,如果 x 不是 所有 1,则返回矩阵向量乘积 A (1 − x);否则 失败。要对向量 b 进行编码,请使用高斯消元法求解 A′ (1 − x′) = b 中的非零 x′,其中 A′ 省略对应的列 到预先存在的一位,x′ 省略对应于 预先存在的一位。如果没有解决方案,就打花边 卡

我没有时间写和验证正式证明,但我的直觉 是,在获取 A 的子矩阵时,我们遇到的概率 线性依赖性随着ε边远离零而迅速降低。

我实现了该算法的更实用版本的模拟 在下面的 C++ 中(可以扩展到编码器/解码器对,而无需 太麻烦了)。它不是全有或全无,而是决定了 它可以控制的最长前缀,并将块用作 6 位长度 其次是该数据量,实现了原始存储的 ≈38%。 长度前缀在这里吃掉了大约 9%(我们控制了 ≈47% 的 bits),但在 large-n 限制中接近 0%。即使使用 64 位 块,你可以通过霍夫曼编码长度来做得更好。

(您可能想知道,如果我们甚至无法控制所有 长度位。我们可以进行设置,以便花边卡解码为 长度 0,这意味着它被跳过,然后打一张花边卡 每当必须发生这种情况时。

#include <algorithm>
#include <cstddef>
#include <iostream>
#include <limits>
#include <optional>
#include <random>
#include <vector>

namespace {

static std::random_device g_dev;

class Vector {
public:
  explicit Vector() = default;
  static Vector Random() {
    return Vector{std::uniform_int_distribution<Bits>{
        0, std::numeric_limits<Bits>::max()}(g_dev)};
  }
  Vector &operator^=(const Vector v) {
    bits_ ^= v.bits_;
    return *this;
  }
  bool operator[](const std::size_t i) const { return bits_ & (Bits{1} << i); }

private:
  using Bits = unsigned long long;
  explicit Vector(const Bits bits) : bits_{bits} {}
  Bits bits_ = 0;
};

class Matrix {
public:
  static Matrix Random(const std::size_t num_rows) {
    Matrix a;
    a.rows_.reserve(num_rows);
    for (std::size_t i = 0; i < num_rows; ++i) {
      a.rows_.push_back(Vector::Random());
    }
    return a;
  }

  Matrix RandomSubsetOfRows(const double p) const {
    Matrix a;
    for (const Vector row : rows_) {
      if (std::bernoulli_distribution{p}(g_dev)) {
        a.rows_.push_back(row);
      }
    }
    return a;
  }

  std::size_t NumControllablePrefixBits() && {
    for (std::size_t j = 0; true; ++j) {
      const auto pivot = [&]() -> std::optional<std::size_t> {
        for (std::size_t i = j; i < rows_.size(); ++i) {
          if (rows_[i][j]) {
            return i;
          }
        }
        return std::nullopt;
      }();
      if (!pivot) {
        return j;
      }
      std::swap(rows_[j], rows_[*pivot]);
      for (std::size_t i = 0; i < rows_.size(); ++i) {
        if (i != j && rows_[i][j]) {
          rows_[i] ^= rows_[j];
        }
      }
    }
  }

private:
  std::vector<Vector> rows_;
};

} // namespace

int main() {
  static constexpr std::size_t kBlocks = 10000;
  static constexpr std::size_t kLogBlockSize = 6;
  static constexpr std::size_t kBlockSize = 1 << kLogBlockSize;
  std::size_t num_usable_bits = 0;
  const Matrix a = Matrix::Random(kBlockSize);
  for (int i = 0; i < kBlocks; ++i) {
    num_usable_bits +=
        a.RandomSubsetOfRows(0.5).NumControllablePrefixBits() - kLogBlockSize;
  }
  std::cout << static_cast<double>(num_usable_bits) / (kBlocks * kBlockSize)
            << "\n";
}

评论

0赞 Dillon Davis 2/7/2023
当我今天晚些时候有机会时,我会很高兴尝试编写这个解决方案。现在,我有一个担忧:编码器和解码器是否必须提前就预先排列的矩阵 A 和 epsilon 值达成一致? 应该不是真正的问题 - 这可以从预先安排的 PRNG 种子确定性地生成。然而,Epsilon 似乎会带来一个问题。解码器如何知道解码器选择的 epsilon?A
0赞 David Eisenstat 2/7/2023
@DillonDavis理论上,epsilon 将与 A 一起提前选择,例如,设定原始容量的 49% 的目标,并且对于 n 个足够大,编码将以很高的概率成功。在实践中,最好是更优雅地失败,特别是如果您使用块变体来提高速度。我会考虑最好的方法。
0赞 David Eisenstat 2/7/2023
@DillonDavis,我添加了一些关于实际实现和C++模拟的注释。
0赞 David Eisenstat 2/7/2023
@DillonDavis 如果你告诉我你的实现限制,很高兴考虑算法工程。
1赞 Dillon Davis 2/9/2023
我只是想让你知道——这并没有从我的雷达上消失,在过去的几天里,我只是遇到了一些事情。我会尽快更详细地查看您的答案和 C++ 代码;我希望能够理解并实施它
2赞 David Eisenstat 2/11/2023 #2

我在 Python 中实现了完整的算法。我用单位矩阵来扩充矩阵,就像矩阵反演算法一样,但将对应于孔的列归零。

import math
import random

log_n = 10
n = log_n + ((1 << log_n) - 1)
matrix = [random.randrange(1 << n) for j in range(n)]


def decode(data):
    text = 0
    for j in range(n):
        if (data & (1 << j)) == 0:
            text ^= matrix[j]
    return text


def invert(existing_data):
    sub = [matrix[j] for j in range(n) if (existing_data & (1 << j)) == 0]
    inverse = [1 << j for j in range(n) if (existing_data & (1 << j)) == 0]
    i = 0
    while True:
        for k in range(i, len(sub)):
            if sub[k] & (1 << i):
                break
        else:
            return inverse[:i]
        sub[i], sub[k] = sub[k], sub[i]
        inverse[i], inverse[k] = inverse[k], inverse[i]
        for k in range(len(sub)):
            if k != i and (sub[k] & (1 << i)):
                sub[k] ^= sub[i]
                inverse[k] ^= inverse[i]
        i += 1


def encode(inverse, text):
    data = ~((~0) << n)
    for i in range(len(inverse)):
        if text & (1 << i):
            data ^= inverse[i]
    return data


def test():
    existing_data = random.randrange(1 << n)
    inverse = invert(existing_data)
    payload_size = max(len(inverse) - log_n, 0)
    payload = random.randrange(1 << payload_size)
    text = payload_size ^ (payload << log_n)
    data = encode(inverse, text)
    assert (existing_data & ~data) == 0

    decoded_text = decode(data)
    decoded_payload_size = decoded_text & (~((~0) << log_n))
    decoded_payload = (decoded_text >> log_n) & (~((~0) << payload_size))
    assert payload_size == decoded_payload_size
    assert payload == decoded_payload
    return payload_size / n


print(sum(test() for i in range(100)))