一文看懂使用C语言实现深度学习嵌入式部署,全流程,超详细

本文以手写数字识别为例,介绍使用C语言实现深度学习嵌入式部署的全流程。主要分为三大部分:Pytorch训练数据集,包括网络模型构建、数据集获取及重建等;C语言实现深度学习操作,如卷积、池化等;嵌入式端构建,提供了创建界面和图片处理的思路。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

这里以手写数字识别为例,全流程代码实现,使用C语言实现卷积、池化、Relu、全连接、Log_Sigmod、softmax层,篇章主要分三大部分:pytorch训练、C语言实现深度学习操作、嵌入式端构建。(三个部分会有一定程度上的交叉,中间有疑问可以先记着,往后看就通了)

觉得还行的话点个赞呗

Pytorch训练数据集:

网络模型构建:

通常来说手写数字识别的神经网络只需要两层全连接层就够了,但是为了实现大多数网络模型的常用操作,我们这里加入了卷积、池化等操作:

此网络结构以两层卷积、以及全连接层构成,中间穿插池化以及ReLu激活函数。

class Net_2(nn.Module):
    def __init__(self):
        super(Net_2, self).__init__()
        self.conv1 = nn.Conv2d(1, 10, kernel_size=5)
        self.conv2 = nn.Conv2d(10, 20, kernel_size=5)
        self.fc1 = nn.Linear(320, 50)
        self.fc2 = nn.Linear(50, 10)
        self.flatten = nn.Flatten()
        self.softmax = nn.LogSoftmax(dim=1)

    def forward(self, x):
        x = self.conv1(x)
        x = F.relu(F.max_pool2d(x, 2))
        x = self.conv2(x)
        x = F.relu(F.max_pool2d(x, 2))
        x = self.flatten(x)
        x = F.relu(self.fc1(x))
        x = self.fc2(x)
        return self.softmax(x)

数据集获取及重建:

数据集我们使用pytorch深度学习中自带的MNIST数据集,此数据集中包含大量书写数字数据,包括训练集和测试集。

在官方文档中,此处获取数据是需要进行标准化的,但是我后面需要进行数据类型转换,为了不影响参数,就先不对数据进行转换,知识单纯读取出数据并转换为张量类型

# 为了后面修改数据类型,这里不进行标准化
transform = torchvision.transforms.Compose([
                                            torchvision.transforms.ToTensor(),
                                            # torchvision.transforms.Normalize((0.1307,),(0.3081,))
                                            ])
# 训练集数据构建
train_loader = torch.utils.data.DataLoader(
    torchvision.datasets.MNIST('./data/',train=True,download=False,transform=transform),
    batch_size=64,
    shuffle=False
)
# 测试集数据构建
test_loader = torch.utils.data.DataLoader(
    torchvision.datasets.MNIST('./data/',train=False,download=False,transform=transform),
    batch_size=1000,
    shuffle=False
)

数据转换并构建自己的数据集:

MNIST数据集的原始图像为0~1的单通道浮点型,为了契合我们后期在嵌入式上方便部署,在这里将数据二值化处理,0.3是二值化阈值,可以根据自己的需求更改。

原始图像及图像:

转换的图像及数据:

# 重构数据获取方式
class My_DataSet(Dataset):
    def __init__(self, datas, labels):
        self.datas = datas
        self.labels = labels

    def __len__(self):
        return len(self.labels)

    def __getitem__(self, idx):
        data = self.datas[idx]
        label = self.labels[idx]
        return data, label

# 构建待存储数据张量
train_data = torch.tensor([])
train_label = torch.tensor([],dtype=torch.int64)
valid_data = torch.tensor([])
valid_label = torch.tensor([],dtype=torch.int64)

# 类型转换,原始图像是0-1的单通道浮点型数据,0.3是阈值
for data, label in train_loader:
    new_data = torch.where(data >= 0.3, torch.tensor(1.), torch.tensor(0.))
    train_data = torch.cat([train_data,new_data],dim=0)
    train_label = torch.cat([train_label,label],dim=0)

for data, label in test_loader:
    new_data = torch.where(data >= 0.3, torch.tensor(1.), torch.tensor(0.))
    valid_data = torch.cat([valid_data,new_data],dim=0)
    valid_label = torch.cat([valid_label,label],dim=0)


# 数据获取类实例化
Train_Data = My_DataSet(train_data,train_label)
Test_Data = My_DataSet(valid_data,valid_label)

# 数据获取
train_iter = DataLoader(Train_Data,batch_size=128,shuffle=True)
test_iter = DataLoader(Test_Data,batch_size=1000,shuffle=False)

开始训练并保存模型:

这里都是深度学习训练的基本操作了,不做过多赘述,会保存下来测试集准确率最高的以此模型。

def train(epoch):
    model.train()
    running_loss = 0.0
    running_corrects = 0
    for batch_idx, (data, label) in enumerate(train_iter):
        data = data.to(device)
        label = label.to(device)
        optimizer_ft.zero_grad()
        with torch.set_grad_enabled(True):
            outputs = model(data)
            loss = criterion(outputs,label)
            _, preds = torch.max(outputs,1)

            loss.backward()
            optimizer_ft.step()

        running_loss += loss.item() * data.size(0)
        running_corrects += torch.sum((preds == label.data))


    epoch_loss = running_loss / len(train_iter.dataset)
    epoch_acc = running_corrects.double() / len(train_iter.dataset) * 100
    scheduler.step(np.mean(epoch_loss))
    print('Train:{} Loss: {:.4f} Acc: {:.4f} Lr: {}'.format(epoch, epoch_loss, epoch_acc, optimizer_ft.param_groups[0]['lr']))

    train_acc_history.append(epoch_acc.to("cpu"))
    train_losses.append(epoch_loss)

def valid():
    global best_acc
    model.eval()
    running_loss = 0.0
    running_corrects = 0
    with torch.no_grad():
        for batch_idx, (data, label) in enumerate(test_iter):
            data = data.to(device)
            label = label.to(device)

            outputs = model(data)
            loss = criterion(outputs,label)

            _, preds = torch.max(outputs, 1)

            running_loss += loss.item() * data.size(0)
            running_corrects += torch.sum((preds == label.data))

        epoch_loss = running_loss / len(test_iter.dataset)
        epoch_acc = running_corrects.double() / len(test_iter.dataset) * 100

        print('\nTest set: Avg. loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format(
            epoch_loss, running_corrects, len(test_iter.dataset),
            epoch_acc))

        if epoch_acc > best_acc:
            best_acc = epoch_acc
            torch.save(model.state_dict(),'number_2.pth')
            print('Renew Model!!!')

        val_acc_history.append(epoch_acc.to("cpu"))
        valid_losses.append(epoch_loss)


train_on_gpu = torch.cuda.is_available()
if not train_on_gpu:
    print("CUDA is training on CPU...")
else:
    print("CUDA is training on GPU...")
device = torch.device("cuda:0" if train_on_gpu else "cpu")

val_acc_history = []
train_acc_history = []
train_losses = []
valid_losses = []
best_acc = 0

model = Net_2().to(device)
optimizer_ft = optim.Adam(model.parameters(),lr=0.001)      #优化器设置
scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer_ft, mode='min', factor=0.8, patience=5, verbose=True, threshold=0.00001, threshold_mode='rel', cooldown=3, min_lr=0, eps=1e-08)
criterion = nn.NLLLoss()

for epoch in range(150):
    train(epoch)
    valid()

效果还行,基本在98%以上

将模型数据转换为C语言可读的格式:

首先先来对比一下pytorch模型数据和C语言需要的数据:

# pytorch数据:
name1:  tensor([  数据1,    数据2, ...,   数据n],\n  [  数据1,    数据2, ...,   数据n],\n  ....)

# C语言需要的数据:
float name1[] = {数据1, 数据2, 数据3, ......, 数据n};

对比发现,我们需要干的是如下几件事:

  1. 提出数据名作为C的变量名
  2. 将数据拍扁,作为一个一维数组
  3. 将tensor内容转换为字符串,并只保留数据部分
  4. 删掉'\n',并且为了美观,将多个连续的空格转换为单空格

然后就可以开始操作了,以第二层的卷积偏置为对象来演示:

开始前需要设置torch的显示属性,分别是科学技术法和数据显示大小,科学计数法没有影响但是看着不自然,数据内容显示量要设置为无穷大,否则会有‘...’:

float fc1_weight[] = {-0.0435, 0.0083, 0.0253, ..., -0.1570, 0.0342, 0.0154};
float conv1_weight[] = {-1.1184e-01, 6.3655e-02};
torch.set_printoptions(sci_mode=False, threshold=torch.inf)
model = torch.load('./number.pth')    #读取模型

#C语言数据起始段
inp = 'float ' + param_tensor.replace('.','_') + '[]' + ' = {'
#float layer_category[] = {


#将多维数据摊平为一维
data = model['conv2.bias'].view(-1)
# tensor([ 0.1306, -0.0828, -0.0754, -0.0553,  0.0371,  0.1295,  0.0217,  0.1280,
#          0.0367, -0.1206,  0.0187, -0.2399, -0.0774, -0.2128, -0.0714,  0.0386,
#          0.0130, -0.1668, -0.0499, -0.1803], device='cuda:0')

# 转换为字符串形式
param_s = str(data.cpu())

# 只保留字符串中[之后和]之前的数据
param_s = param_s.split('[')[1].split(']')[0]
# '0.1306, -0.0828, -0.0754, -0.0553,  0.0371,  0.1295,  0.0217,  0.1280,  0.0367, -0.1206,  0.0187, -0.2399, -0.0774, -0.2128, -0.0714,  0.0386,  0.0130, -0.1668, -0.0499, -0.1803
#'

#删掉多余的\n
param_s = param_s.replace("\n", "")

#使用正则表达式将连续的多个空格转换为单空格
param_s = re.sub(r'\s+', ' ', param_s)

inp += param_s

#加尾缀并换行
inp += '};\r\n'

完整过程:

model = torch.load('./number.pth')

inp = ''
for param_tensor in model:
    inp = 'float ' + param_tensor.replace('.','_') + '[]' + ' = {'
    load = model[param_tensor].view(-1)
    param_s = str(load.cpu())
    param_s = param_s.split('[')[1].split(']')[0]

    param_s = param_s.replace("\n", "")
    param_s = re.sub(r'\s+', ' ', param_s)

    inp += param_s
    inp += '};\r\n'

    with open('./para2.h', 'a+', encoding='utf-8') as f:
        f.write(inp)

至此,Python部分全部完成。

C语言实现深度学习操作:

卷积:

首先,卷积就是点乘相加的过程:

单通道卷积:

多通道卷积:

多通道卷积则是每个卷积核与对应的通道点乘,在将每个通道的卷积结果相加再加偏置。

在清楚了这个之后我们再看pytorch的卷积数据构建形式。

这里我们定义的一个三通道3*3大小的输入数据,卷积核大小为2,输出通道为2,为了便于演示和计算,卷积核参数以及偏置参数我们自己设置。

Input = [
    [[0,0,0],
    [0,1,1],
    [0,1,1]],

    [[0,0,0],
    [0,0,1],
    [0,2,2]],

    [[0,0,0],
    [0,2,2],
    [0,0,0]]
]

Weight = [
    [[[1,1],
    [-1,-1]],

    [[-1,-1],
    [-1,1]],

    [[1,0],
    [0,0]]],

    [[[0,0],
    [-1,1]],

    [[0,0],
    [1,0]],

    [[-1,1],
    [0,-1]]]
]
Bias = [1,2]

Weight = torch.tensor(Weight,dtype=torch.float)
Bias = torch.tensor(Bias,dtype=torch.float)
Input = torch.tensor(Input,dtype=torch.float)

conv1 = nn.Conv2d(3,2,kernel_size=2)
conv1.weight = nn.Parameter(Weight)
conv1.bias = nn.Parameter(Bias)

print(Input.shape)
# torch.Size([3, 3, 3])
print(conv1.weight.shape)
# torch.Size([2, 3, 2, 2])
print(conv1.bias.shape)
# torch.Size([2])

out = conv1(Input)
print(out.shape)
# torch.Size([2, 2, 2])
print(out)

tensor([[[0., 0.],
         [3., 2.]],

        [[1., 0.],
         [5., 4.]]], grad_fn=<SqueezeBackward1>)

卷积的权重参数结构为[2, 3, 2, 2],可以看出,pytorch的参数结构相比于tensorflow更人性化,此计算过程可以这样理解:

现在我想要计算输入特征第三个通道第二个输出特征的卷积结果,那么在权重参数维度上的选择就是,在第一个维度上选择1(数组索引,后续一样),在第二个维度上选择2,那么就会取得一个2*2的卷积核[[  ,   ], [  ,  ]],就可以用此与输入特征对应位置进行点乘。

在我们上述内容中有对维度摊平操作,也就是在摊平后的维度是[24]。在计算机内部本身是不存在数组维度一说,一个数组中的所有数据都是连续的,只是为了便于操作才加入的维度及对应索引。

在清楚数据计算方式后我们就可以使用C语言开始实现多通道卷积操作,在函数中将ReLu作为同时计算,就不用再单独设立一个函数了:

unsigned int count = 0;
/****************************************************************************************/
//img: 输入特征
//Img_Size: 输入特征单通道的尺寸,如3*28*28,那么就是3通道尺寸大小为28的特征
//w:卷积权重参数
//In_Ch:输入通道数
//Out_Ch:输出通道数
//Kernel_Size:卷积核大小
//out:输出特征
//Out_Size:输出特征单通道尺寸大小
//use_relu:是否使用ReLu激活函数。0不使用,1使用
/****************************************************************************************/
void convs_f(float* img, int Img_Size,
    float* w, int In_Ch, int Out_Ch, int Kernel_Size,
    float* b,
    float* out, int Out_Size,
    char use_relu)
{   

    float tmp = 0.0, tmp1 = 0.0;
    for (int m = 0; m < Out_Ch; m++)
    {
        for (int k = 0; k <= Img_Size - Kernel_Size; k++)  //特征平面的行  列平移 行卷积
        {

            for (int r = 0; r <= Img_Size - Kernel_Size; r++) //特征平面的列  行平移  列卷积
            {
                //多通道累加
                tmp1 = 0.0;
                for (int n = 0; n < In_Ch; n++)
                {
                    tmp = 0.0;
                    //单次卷积 点对点相乘 然后相加
                    for (int i = 0; i < Kernel_Size; i++) //卷积的行
                    {
                        for (int j = 0; j < Kernel_Size; j++) //卷积的列
                        {
                            tmp += *(img + n * (Img_Size * Img_Size) + (i + k) * Img_Size + j + r) * *(w + m * (Kernel_Size * Kernel_Size * In_Ch) + n * (Kernel_Size * Kernel_Size) + i * Kernel_Size + j);
                            /*tmp += img[n][i + k][j + r] * w[m][n][i][j];*/
                        }
                    }
                    //累加多个特征平面的卷积结果
                    tmp1 += tmp;
                }
                if (use_relu == 0) *(out + m * Out_Size * Out_Size + k * Out_Size + r) = tmp1 + b[m];
                else if(use_relu == 1) *(out + m * Out_Size * Out_Size + k * Out_Size + r) = tmp1 + b[m] >= 0 ? tmp1 + b[m] : 0;
                count++;
            }
        }
    }
}

既然使用了C语言,那么C语言最大的魅力莫过于指针,因此,在清楚数据结构计算方式后,所有的参数均可以用一级指针(也就是一维数组)来操作。这样就可以提高函数的可用性并且还避免了多级指针中需要多次malloc再free的繁琐操作。

池化(max_pooling):

池化操作和卷积相近,只是不需要有卷积核参数计算。

/****************************************************************************************/
//img: 输入特征
//Img_Size: 输入特征单通道的尺寸,如3*28*28,那么就是3通道大小为尺寸大小为28的特征
//Img_Ch:输入通道数
//Out_Ch:输出通道数
//out:输出特征
//Kernel_Size:卷积核大小
//use_relu:是否使用ReLu激活函数。0不使用,1使用
/****************************************************************************************/
void max_pooling_f(float *img, int Img_Size, int Img_Ch, float *out, int Out_Size,int Kernel_Size, char use_relu)
{
    int i, j, k, r, z;
    float tmp1, tmp2, tmp3;
    for (z = 0; z < Img_Ch; z++) {
        for (i = 0, k = 0; i < Img_Size; i = i + Kernel_Size, k++)
        {
            for (j = 0, r = 0; j < Img_Size; j = j + Kernel_Size, r++)
            {
                tmp1 = 0.0f;
                tmp2 = 0.0f;
                tmp3 = 0.0f;
                
                tmp1 = *(img+ z * (Img_Size * Img_Size) + i * Img_Size + j) > *(img + z * (Img_Size * Img_Size) + i * Img_Size + j + 1) ? *(img + z * (Img_Size * Img_Size) + i * Img_Size + j) : *(img + z * (Img_Size * Img_Size) + i * Img_Size + j + 1);
                tmp2 = *(img + z * (Img_Size * Img_Size) + (i + 1) * Img_Size + j) > *(img + z * (Img_Size * Img_Size) + (i + 1) * Img_Size + j + 1) ? *(img + z * (Img_Size * Img_Size) + (i + 1) * Img_Size + j) : *(img + z * (Img_Size * Img_Size) + (i + 1) * Img_Size + j + 1);
                tmp3 = tmp1 > tmp2 ? tmp1 : tmp2;

                if(use_relu == 0) *(out + z * (Out_Size * Out_Size) + k * Out_Size + r) = tmp3;
                else if(use_relu == 1) *(out + z * (Out_Size * Out_Size) + k * Out_Size + r) = tmp3 >= 0 ? tmp3 : 0;
                count++;
            }
        }
    }
    return 0;
}

全连接层: 

/****************************************************************************************/
//img: 输入特征
//In_Feature: 输入特征参数
//w:权重参数
//b:偏置参数
//Out:输出特征
//Out_Feature:输出特征参数
//use_relu:是否使用ReLu激活函数。0不使用,1使用
/****************************************************************************************/
void Linear(float* Img, int In_Feature, float* w, float* b, float *Out,int Out_Feature, char use_relu)
{
    float tmp = 0;
    int i = 0, j = 0;

    for (i = 0; i < Out_Feature; i++) {
        tmp = 0;
        for (j = 0; j < In_Feature; j++) {
            tmp += *(Img + j) * *(w + i * In_Feature + j);
        }
        tmp += *(b + i);

        if (use_relu == 0) *(Out + i) = tmp;
        else if (use_relu == 1) *(Out + i) = tmp >= 0 ? tmp : 0;
        count++;
    }
}

Log_Softmax层:

void log_softmax(float* input, int size) {
    // 计算原始的 softmax
    float max_val = input[0];
    for (int i = 1; i < size; ++i) {
        if (input[i] > max_val) {
            max_val = input[i];
        }
    }

    float sum_exp = 0.0;
    for (int i = 0; i < size; ++i) {
        input[i] = expf(input[i] - max_val);
        sum_exp += input[i];
    }

    // 计算对数 Softmax
    for (int i = 0; i < size; ++i) {
        input[i] = logf(input[i] / sum_exp);
    }
}

获取最大置信度索引:

char find_max(float* input, int size) {
    int i = 0, index_target = 0, max_val = *input;

    for (int i = 1; i < size; i++) {
        if (*(input+i) > max_val) {
            max_val = *(input + i);
            index_target = i;
        }
    }
    return index_target;
}

嵌入式端构建:

由于不同设备的不同,这边我只提供一个思路,作者所用的IMX6ULL,屏幕大小是7寸屏,如果有需要的话可以和我要源码。

首先是简单创建一个界面,分别包括手绘区,开始转换区(右上),界面清除区(右下)

MNIST数据集中的一张图片大小是28*28,实际我们为了便于展示肯定不会这么小,作者此处的绘制区域大小为590*590,画笔大小为30*30,也就是说只需要缩小20倍就可以达到预定要求。

随后用交替行缩小算法将图片缩小至28*28:

这里同时加入数据转换,将32位像素数据点转换为0,1二值化数据,实现方式:

//屏幕尺寸
#define SCREEN_WIDTH    1024
#define SCREEN_HEIGHT   640

//手绘区
#define START_X         0
#define START_Y         0
#define SIZE            590

//缩小尺寸展示区
#define M_START_X       600
#define M_START_Y       200
#define M_SIZE          28
#define SCALE           20

/*********************************************/
//raw_img:原始图像
//change_img:缩小后的图像
//Input:送神经网络的输入数据
//scale:缩小比例
/*********************************************/
void Reduce_Image(unsigned int *raw_img, unsigned int *change_img, float Input[28][28], int scale)
{
    int i, j, row = 0, column = 0;
    for(i = 0; i < SIZE / scale - 1; i++) {//列
        for(j = 0; j < SIZE / scale - 1; j++) {//行
            change_img[i*M_SIZE+j] = raw_img[(START_Y+row)*SCREEN_WIDTH+column];
            Input[i][j] = change_img[i*M_SIZE+j] == 0 ? 0. : 1.;
            column += scale;
        }
        row += scale;
        column = 0;
    }
}
float Input[28][28];
float Out[10][24][24];
float Out_Pool[10][12][12];
float Out_Conv2[20][8][8];
float Out_Pool2[20][4][4];
float Out_Fc1[50];
float Out_Fc2[10];

if(sample.x >= 800 && sample.x <= 900 && sample.y >= START_Y && sample.y <= START_Y + 100){
                    printf("开始转换\r\n");
                    Reduce_Image(screen.screen_base,min_img,Input,SCALE);
                    
                    convs_f(Input, 28,conv1_weight, 1, 10, 5,conv1_bias, Out, 24,0);
                    printf("comp: %d\r\n", count);
                    count = 0;

                    max_pooling_f(Out, 24, 10, Out_Pool, 12, 2,1);
                    printf("comp: %d\r\n", count);
                    count = 0;

                    convs_f(Out_Pool, 12, conv2_weight, 10, 20, 5, conv2_bias, Out_Conv2, 8, 0);
                    printf("comp: %d\r\n", count);
                    count = 0;
                    
                    max_pooling_f(Out_Conv2, 8, 20, Out_Pool2, 4, 2, 1);
                    printf("comp: %d\r\n", count);
                    count = 0;

                    Linear(Out_Pool2, 320, fc1_weight, fc1_bias, Out_Fc1, 50, 1);
                    printf("comp: %d\r\n", count);
                    count = 0;
                    
                    Linear(Out_Fc1, 50, fc2_weight, fc2_bias, Out_Fc2, 10, 0);
                    printf("comp: %d\r\n", count);
                    count = 0;

                    log_softmax(Out_Fc2, 10);
                    printf("res: %d \r\n",find_max(Out_Fc2, 10));

当触摸到指定区域后开始转换 ,程序中的count是作为debug用的,实际运行中可以删除,测试一下。

测试没问题。

后续有时间再更新深度学习其他算法的C实现。。。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值