以下是准备LeNet数字识别的单通道卷积网络测试数据的步骤和方法:
图片数据准备
由于LeNet神经网络在训练过程中对输入图像数据进行了归一化处理,因此我们在进行FPGA代码测试时也应对测试图片需要进行归一化处理,常见的做法是将像素值缩放到[0,1]或[-1,1]区间。对于灰度图像,测试图像数据如下。
权重导出为Q8.8格式
Q8.8格式是一种定点数表示方法,其中8位表示整数部分,8位表示小数部分。将浮点权重转换为Q8.8格式的步骤如下:
输入图像先被归一化到0~1浮点范围,通过乘以256并四舍五入转换为Q8.8格式。例如:
- 0.5 → 0.5×256 = 128(即0x0080,表示+0.5)
- 1.0 → 256(即0x0100,表示+1.0)
数值转换细节
np.clip
确保数值在16位有符号整数范围内(-32768~32767)。负值示例:
- -0.5 → -128(存储为0xFF80,二进制补码表示)
每个像素值写入HEX文件时采用4位十六进制小写格式,通过val & 0xFFFF
确保只保留低16位。例如:
- 255 → 0x00ff
- -32768 → 0x8000
测试图片test_0_7.jpg转换数据代码如下:
# img2hex_q8_8.py (28×28 灰度输入,输出 image.hex)
import numpy as np, cv2, argparse
ap = argparse.ArgumentParser()
ap.add_argument("--in", required=True, help="原图像路径")
ap.add_argument("--out", default="image.hex")
args = ap.parse_args()
img = cv2.imread(args.__dict__["in"], cv2.IMREAD_GRAYSCALE)
img = cv2.resize(img, (28,28), interpolation=cv2.INTER_AREA)
img = img.astype(np.float32) / 255.0 # 0~1
img_q = np.round(img * 256).astype(np.int16) # Q8.8
img_q = np.clip(img_q, -32768, 32767)
with open(args.out, "w") as f:
for val in img_q.flatten(): # 行优先
f.write(f"{val & 0xFFFF:04x}\n")
print("写出 HEX OK:", args.out)
权重数据准备
通过解析LeNet模型的state_dict,获取每一层的权重参数。使用Python字典保存不同网络层的参数, 将 LeNet-2 模型的权重参数量化并导出为 HEX 格式文件。量化采用 Q8.8 定点数格式(16 位有符号整数),权重按层分离存储,便于硬件部署或嵌入式系统加载。
重点事项!!!
在FPGA测试过程中,权重数据与图像数据的对应关系至关重要,确保两者顺序一致才能正确完成卷积运算。权重文件的保存顺序通常遵循输出通道优先、输入通道次之、最后是卷积核大小的顺序。这种顺序设计是为了高效利用硬件资源,避免数据访问冲突。
权重保存顺序示例: 输出通道(O) → 输入通道(I) → 卷积核高度(H) → 卷积核宽度(W)
保存文件为:
conv1.weight
从形状 (6,1,5,5) 展平为 150 个元素- conv1.bias 6个元素
conv2.weight
从形状 (16,6,5,5) 展平为 2400 个元素- conv2.bias 16个元素
fc.weight
按行优先展开(10 类×256 维=2560 元素)- 最大池化层用于降低特征图的空间维度,不具备参数文件
#!/usr/bin/env python3
# export_lenet2_onefile_hex.py
# -----------------------------------------
# 将 LeNet-2 权重量化为 Q8.8,并按层导出单一 HEX 文件
# -----------------------------------------
import os, argparse, numpy as np, torch
def quantize_int16(t: torch.Tensor, scale=256):
q = torch.round(t * scale).to(torch.int32)
q = torch.clamp(q, -32768, 32767).to(torch.int16)
return q.cpu().numpy()
def save_hex(arr: np.ndarray, path: str):
arr = arr.flatten()
with open(path, "w") as f:
for v in arr:
f.write(f"{(v & 0xFFFF):04x}\n")
print(f" ✔ {path:<20} {arr.size:>6} words")
def main(pth_path: str, out_dir: str = "hex"):
os.makedirs(out_dir, exist_ok=True)
state = torch.load(pth_path, map_location="cpu")
# ---- Conv1 weight (6,1,5,5) → 150 ----
w1 = state["conv1.weight"].reshape(-1) # OC-major
save_hex(quantize_int16(w1), os.path.join(out_dir, "conv1.weight.hex"))
save_hex(quantize_int16(state["conv1.bias"]), os.path.join(out_dir, "conv1.bias.hex"))
# ---- Conv2 weight (16,6,5,5) → 2400 ----
w2 = state["conv2.weight"].reshape(-1) # OC-major
save_hex(quantize_int16(w2), os.path.join(out_dir, "conv2.weight.hex"))
save_hex(quantize_int16(state["conv2.bias"]), os.path.join(out_dir, "conv2.bias.hex"))
# ---- FC weight (10,256) → 2560 ----
w_fc = state["fc.weight"].reshape(-1) # row-major (class0 行 → class9 行)
save_hex(quantize_int16(w_fc), os.path.join(out_dir, "fc.weight.hex"))
save_hex(quantize_int16(state["fc.bias"]), os.path.join(out_dir, "fc.bias.hex"))
print("\n全部导出完成。")
if __name__ == "__main__":
ap = argparse.ArgumentParser(description="导出 LeNet-2 HEX 权重(Q8.8)")
ap.add_argument("model", help=".pth 权重文件")
ap.add_argument("-o", "--out", default="hex", help="输出目录(默认 hex)")
args = ap.parse_args()
main(args.model, args.out)