中文亚洲精品无码_熟女乱子伦免费_人人超碰人人爱国产_亚洲熟妇女综合网

當(dāng)前位置: 首頁(yè) > news >正文

南聯(lián)網(wǎng)站建設(shè)哪家好seo是什么意思新手怎么做seo

南聯(lián)網(wǎng)站建設(shè)哪家好,seo是什么意思新手怎么做seo,什么渠道做網(wǎng)站建設(shè),網(wǎng)頁(yè)打不開怎么設(shè)置我最近在從事一項(xiàng)很有意思的項(xiàng)目,我想在PFGA上部署CNN并實(shí)現(xiàn)手寫圖片的識(shí)別。而本篇文章,是我邁出的第二步。具體代碼已發(fā)布在github上 模塊介紹 卷積神經(jīng)網(wǎng)絡(luò)(CNN)可以分為卷積層、池化層、激活層、全鏈接層結(jié)構(gòu)。本篇實(shí)現(xiàn)的,就…

我最近在從事一項(xiàng)很有意思的項(xiàng)目,我想在PFGA上部署CNN并實(shí)現(xiàn)手寫圖片的識(shí)別。而本篇文章,是我邁出的第二步。具體代碼已發(fā)布在github上

模塊介紹

卷積神經(jīng)網(wǎng)絡(luò)(CNN)可以分為卷積層、池化層、激活層、全鏈接層結(jié)構(gòu)。本篇實(shí)現(xiàn)的,就是CNN的卷積層中的卷積運(yùn)算模塊。

卷積運(yùn)算的過(guò)程如下圖所示:

img

在權(quán)重參數(shù)已經(jīng)確定的情況下,我們可以將這過(guò)程看成數(shù)據(jù)滑窗卷積運(yùn)算的這兩個(gè)步驟的重復(fù)運(yùn)算。在前文中,我們已經(jīng)實(shí)現(xiàn)了window模塊,而此處我們實(shí)現(xiàn)卷積運(yùn)算模塊。

運(yùn)算過(guò)程如下:
[ 1 2 3 4 5 6 7 8 9 ] ? [ 1 0 1 0 1 0 1 1 2 ] = 1 ? 1 + 2 ? 0 + 3 ? 1 + 4 ? 0 + 5 ? 1 + 6 ? 0 + 7 ? 1 + 8 ? 1 + 9 ? 2 = 42 \begin{bmatrix}1&2&3\\4&5&6\\7&8&9\end{bmatrix} \ast \begin{bmatrix}1&0&1\\ 0&1&0\\1&1&2 \end{bmatrix}=1\cdot1 +2 \cdot0 +3\cdot1+4\cdot0+5\cdot 1+6\cdot0+7\cdot1+8\cdot 1+9\cdot 2 \\ =42 ?147?258?369? ?? ?101?011?102? ?=1?1+2?0+3?1+4?0+5?1+6?0+7?1+8?1+9?2=42

代碼

  1. 模塊可配置參數(shù)、輸入和輸出定義

為了支持多通道并行處理,輸入為所有輸入通道展平后的數(shù)據(jù),如一維的窗口數(shù)據(jù)和權(quán)重參數(shù)

DATA_WIDTH和WEIGHT_WIDTH分開定義,因?yàn)楹罄m(xù)工作中會(huì)對(duì)權(quán)重定點(diǎn)數(shù)量化

module mult_acc_comb #(parameter DATA_WIDTH = 8,parameter KERNEL_SIZE = 3,parameter IN_CHANNEL = 3,parameter WEIGHT_WIDTH = 8,parameter OUTPUT_WIDTH = 20,  // 可配置的輸出位寬parameter ACC_WIDTH = 2*DATA_WIDTH + 4 + $clog2(KERNEL_SIZE*KERNEL_SIZE*IN_CHANNEL) // Ensure ACC_WIDTH is sufficient
)(// 輸入數(shù)據(jù)接口input window_valid,input [IN_CHANNEL*KERNEL_SIZE*KERNEL_SIZE*DATA_WIDTH-1:0] multi_channel_window_in,input weight_valid,input [IN_CHANNEL*KERNEL_SIZE*KERNEL_SIZE*WEIGHT_WIDTH-1:0] multi_channel_weight_in,// 輸出數(shù)據(jù)接口output [OUTPUT_WIDTH-1:0] conv_out, // 使用可配置的輸出位寬output conv_valid
);
  1. 定義內(nèi)部相關(guān)信號(hào)
// 計(jì)算權(quán)重相關(guān)參數(shù)
localparam WEIGHTS_PER_FILTER = IN_CHANNEL * KERNEL_SIZE * KERNEL_SIZE;// 解包后的多通道窗口數(shù)據(jù)和權(quán)重?cái)?shù)據(jù),無(wú)符號(hào)
wire [DATA_WIDTH-1:0] channel_window_data [0:IN_CHANNEL-1][0:KERNEL_SIZE*KERNEL_SIZE-1]; 
wire [WEIGHT_WIDTH-1:0] channel_weight_data [0:IN_CHANNEL-1][0:KERNEL_SIZE*KERNEL_SIZE-1];  // 每個(gè)通道每個(gè)位置的乘法結(jié)果,無(wú)符號(hào)
wire [DATA_WIDTH+WEIGHT_WIDTH-1:0] mult_results [0:IN_CHANNEL-1][0:KERNEL_SIZE*KERNEL_SIZE-1]; // 每個(gè)通道的累加結(jié)果
wire [ACC_WIDTH-1:0] channel_sums [0:IN_CHANNEL-1];// 最終跨通道累加結(jié)果
wire [ACC_WIDTH-1:0] total_sum; // 循環(huán)變量
genvar ch, i_idx, k_idx, c_idx; 
  1. 輸入數(shù)據(jù)解包
generatefor (ch = 0; ch < IN_CHANNEL; ch = ch + 1) begin : unpack_genfor (i_idx = 0; i_idx < KERNEL_SIZE*KERNEL_SIZE; i_idx = i_idx + 1) begin : element_gen// 解包窗口數(shù)據(jù)assign channel_window_data[ch][i_idx] = multi_channel_window_in[(ch*KERNEL_SIZE*KERNEL_SIZE + i_idx)*DATA_WIDTH +: DATA_WIDTH ];// 解包權(quán)重?cái)?shù)據(jù)assign channel_weight_data[ch][i_idx] = multi_channel_weight_in[(WEIGHTS_PER_FILTER - 1 - (ch*KERNEL_SIZE*KERNEL_SIZE + i_idx))*WEIGHT_WIDTH +: WEIGHT_WIDTH];endend
endgenerate

a[ b +: c ]的含義是,從a的b位,向上提取c位,也就是a[b+c:b+1];

輸入的window和weight的數(shù)據(jù)結(jié)構(gòu)變化如下

在這里插入圖片描述

  1. 并行卷積運(yùn)算

所有通道同時(shí)進(jìn)行卷積運(yùn)算

// 并行乘法 - 所有通道所有位置同時(shí)計(jì)算
generatefor (ch = 0; ch < IN_CHANNEL; ch = ch + 1) begin : mult_ch_genfor (i_idx = 0; i_idx < KERNEL_SIZE*KERNEL_SIZE; i_idx = i_idx + 1) begin : mult_elem_genassign mult_results[ch][i_idx] = channel_window_data[ch][i_idx] * channel_weight_data[ch][i_idx];endend
endgenerate// 每個(gè)通道內(nèi)累加 - 使用組合邏輯加法樹
generatefor (ch = 0; ch < IN_CHANNEL; ch = ch + 1) begin : sum_ch_genif (KERNEL_SIZE == 3) begin : kernel3_sumassign channel_sums[ch] = mult_results[ch][0] + mult_results[ch][1] + mult_results[ch][2] +mult_results[ch][3] + mult_results[ch][4] + mult_results[ch][5] +mult_results[ch][6] + mult_results[ch][7] + mult_results[ch][8];end else begin : general_sumwire [ACC_WIDTH-1:0] partial_sums [0:KERNEL_SIZE*KERNEL_SIZE-1];assign partial_sums[0] = mult_results[ch][0];for (k_idx = 1; k_idx < KERNEL_SIZE*KERNEL_SIZE; k_idx = k_idx + 1) begin : acc_genassign partial_sums[k_idx] = partial_sums[k_idx-1] + mult_results[ch][k_idx];endassign channel_sums[ch] = partial_sums[KERNEL_SIZE*KERNEL_SIZE-1];endend
endgenerate
  1. 跨通道累加并輸出

對(duì)所有通道結(jié)果進(jìn)行相加,進(jìn)行飽和處理,然后輸出

// 跨通道累加 - 組合邏輯
generateif (IN_CHANNEL == 3) begin : channel3_sumassign total_sum = channel_sums[0] + channel_sums[1] + channel_sums[2];end else begin : general_channel_sumwire [ACC_WIDTH-1:0] channel_partial_sums [0:IN_CHANNEL-1];assign channel_partial_sums[0] = channel_sums[0];for (c_idx = 1; c_idx < IN_CHANNEL; c_idx = c_idx + 1) begin : ch_acc_genassign channel_partial_sums[c_idx] = channel_partial_sums[c_idx-1] + channel_sums[c_idx];endassign total_sum = channel_partial_sums[IN_CHANNEL-1];end
endgenerate// 輸出邏輯 - 組合邏輯
assign conv_valid = window_valid && weight_valid;
assign conv_out = conv_valid ? saturate(total_sum) : {OUTPUT_WIDTH{1'b0}};// 飽和處理函數(shù)(組合邏輯)- UNSIGNED
function [OUTPUT_WIDTH-1:0] saturate;input [ACC_WIDTH-1:0] value; // UNSIGNEDlocalparam [ACC_WIDTH-1:0] MAX_UNSIGNED_VAL_SAT = (1 << OUTPUT_WIDTH) - 1;// MIN_UNSIGNED_VAL is 0beginif (value > MAX_UNSIGNED_VAL_SAT)saturate = MAX_UNSIGNED_VAL_SAT[OUTPUT_WIDTH-1:0]; // 使用OUTPUT_WIDTH進(jìn)行截取elsesaturate = value[OUTPUT_WIDTH-1:0]; // 使用OUTPUT_WIDTH進(jìn)行截取end
endfunction

測(cè)試

mult_acc_comb_tb.v

為驗(yàn)證其功能性,使用多個(gè)case經(jīng)行測(cè)試,并對(duì)比結(jié)果

`timescale 1ns / 1psmodule mult_acc_comb_tb;parameter DATA_WIDTH = 8;
parameter KERNEL_SIZE = 3;
parameter IN_CHANNEL = 3;
parameter WEIGHT_WIDTH = 8;
parameter OUTPUT_WIDTH = 20;  
parameter ACC_WIDTH = 2*DATA_WIDTH + 4 + $clog2(KERNEL_SIZE*KERNEL_SIZE*IN_CHANNEL);reg window_valid;
reg [IN_CHANNEL*KERNEL_SIZE*KERNEL_SIZE*DATA_WIDTH-1:0] multi_channel_window_in;
reg weight_valid;
reg [IN_CHANNEL*KERNEL_SIZE*KERNEL_SIZE*WEIGHT_WIDTH-1:0] multi_channel_weight_in;wire [OUTPUT_WIDTH-1:0] conv_out;
wire conv_valid;localparam MAX_UNSIGNED_OUT_VAL = (1 << OUTPUT_WIDTH) - 1;// Example: Test 2 raw sum for unsigned context
localparam EXPECTED_SUM_TEST2_UNSIGNED_RAW = 3 * 9 * 2 * 3; // 162
localparam EXPECTED_CONV_OUT_TEST2_UNSIGNED_SAT = (EXPECTED_SUM_TEST2_UNSIGNED_RAW > MAX_UNSIGNED_OUT_VAL) ? MAX_UNSIGNED_OUT_VAL : EXPECTED_SUM_TEST2_UNSIGNED_RAW;
localparam MAX_ELEMENT_VAL_TB = (1 << DATA_WIDTH) -1;
localparam MAX_WEIGHT_ELEMENT_VAL_TB = (1 << WEIGHT_WIDTH) -1;mult_acc_comb #(.DATA_WIDTH(DATA_WIDTH),.KERNEL_SIZE(KERNEL_SIZE),.IN_CHANNEL(IN_CHANNEL),.WEIGHT_WIDTH(WEIGHT_WIDTH),.OUTPUT_WIDTH(OUTPUT_WIDTH),.ACC_WIDTH(ACC_WIDTH)
) dut (.window_valid(window_valid),.multi_channel_window_in(multi_channel_window_in),.weight_valid(weight_valid),.multi_channel_weight_in(multi_channel_weight_in),.conv_out(conv_out),.conv_valid(conv_valid)
);reg all_tests_passed_flag; 
integer test_id_counter;
integer num_errors;// Task to check results and display Expected/Actual for all
task check_and_report;input [OUTPUT_WIDTH-1:0] expected_out_val;input expected_valid_val;// Test description is displayed before calling this taskbegintest_id_counter = test_id_counter + 1;// Always display Expected and Actual$display("    Expected: conv_valid=%b, conv_out=%d", expected_valid_val, expected_out_val);$display("    Actual:   conv_valid=%b, conv_out=%d", conv_valid, conv_out);if (conv_valid === expected_valid_val &&( (expected_valid_val === 1'b0) ? (conv_out === {OUTPUT_WIDTH{1'b0}}) : (conv_out === expected_out_val) ) ) begin$display("    Test ID %0d: Status: PASSED", test_id_counter);end else begin$display("    Test ID %0d: Status: FAILED", test_id_counter);all_tests_passed_flag = 1'b0;num_errors = num_errors + 1;end$display("--------------------------------------------------");end
endtaskinitial begin$display("=== Comprehensive UNSIGNED Combinational MultAcc Test (OUTPUT_WIDTH=%0d) ===", OUTPUT_WIDTH);all_tests_passed_flag = 1'b1; test_id_counter = 0;num_errors = 0;// Initializewindow_valid = 0;weight_valid = 0;multi_channel_window_in = 0;multi_channel_weight_in = 0;#10;// Test 1$display("Test Description: Simple Positive Values (1*1, sum 27)");multi_channel_window_in = {27{8'd1}}; multi_channel_weight_in = {27{8'd1}}; window_valid = 1;weight_valid = 1;#1; check_and_report(27, 1'b1);#10;// Test 2$display("Test Description: Positive Values with Saturation (2*3, raw %0d, sat %0d)", EXPECTED_SUM_TEST2_UNSIGNED_RAW, EXPECTED_CONV_OUT_TEST2_UNSIGNED_SAT);multi_channel_window_in = {27{8'd2}};multi_channel_weight_in = {27{8'd3}};#1; check_and_report(EXPECTED_CONV_OUT_TEST2_UNSIGNED_SAT, 1'b1);#10;// Test 3$display("Test Description: Invalid Inputs (both valid_n low)");window_valid = 0;weight_valid = 0;#1;check_and_report(0, 1'b0); #10;// Test 4$display("Test Description: Zero Window Data, Non-zero Weights");window_valid = 1;weight_valid = 1;multi_channel_window_in = {27{8'd0}}; multi_channel_weight_in = {27{8'd5}}; #1;check_and_report(0, 1'b1);#10;// Test 5$display("Test Description: Non-zero Window, Zero Weight Data");multi_channel_window_in = {27{8'd5}}; multi_channel_weight_in = {27{8'd0}}; #1;check_and_report(0, 1'b1);#10;// Test 6$display("Test Description: All Zero Inputs");multi_channel_window_in = {27{8'd0}}; multi_channel_weight_in = {27{8'd0}}; #1;check_and_report(0, 1'b1);#10;// Test 7$display("Test Description: Large values (no saturation with 20-bit output)");multi_channel_window_in = {27{8'd5}}; multi_channel_weight_in = {27{8'd5}}; #1;check_and_report(27*5*5, 1'b1);  // 27*25 = 675, well within 20-bit range#10;// Test 8$display("Test Description: Max Val Inputs (Win=%d, Wgt=%d), should saturate to %d", MAX_ELEMENT_VAL_TB, MAX_WEIGHT_ELEMENT_VAL_TB, MAX_UNSIGNED_OUT_VAL);multi_channel_window_in = {27{{DATA_WIDTH{1'b1}}}};multi_channel_weight_in = {27{{WEIGHT_WIDTH{1'b1}}}};#1;// 27 * 255 * 255 = 1,759,725, which exceeds 20-bit max (1,048,575), so should saturatecheck_and_report(MAX_UNSIGNED_OUT_VAL, 1'b1);#10;// Test 8.5: Test 20-bit range capability$display("Test Description: Medium values to test 20-bit range (100*100, sum 270000)");multi_channel_window_in = {27{8'd100}}; multi_channel_weight_in = {27{8'd100}}; #1;check_and_report(27*100*100, 1'b1);  // 27*10000 = 270000, well within 20-bit range#10;// Test 9: Window valid toggles$display("--- Test Sequence 9: Window Valid Toggles (base inputs 1*1, sum 27) ---");multi_channel_window_in = {27{8'd1}};multi_channel_weight_in = {27{8'd1}};weight_valid = 1; $display("  Sub-Test Description: WinValid=1 (Start)");window_valid = 1; #1; check_and_report(27, 1'b1);$display("  Sub-Test Description: WinValid=0");window_valid = 0; #1; check_and_report(0,  1'b0);$display("  Sub-Test Description: WinValid=1 (End)");window_valid = 1; #1; check_and_report(27, 1'b1);#10;// Test 10: Weight valid toggles$display("--- Test Sequence 10: Weight Valid Toggles (base inputs 1*1, sum 27) ---");window_valid = 1; // inputs are still 1s$display("  Sub-Test Description: WeightValid=1 (Start)");weight_valid = 1; #1; check_and_report(27, 1'b1);$display("  Sub-Test Description: WeightValid=0");weight_valid = 0; #1; check_and_report(0,  1'b0);$display("  Sub-Test Description: WeightValid=1 (End)");weight_valid = 1; #1; check_and_report(27, 1'b1);#10;// Final Summary$display("==================================================");if (all_tests_passed_flag) begin$display("FINAL STATUS: SUCCESS! All %0d UNSIGNED Combinational MultAcc tests passed!", test_id_counter);end else begin$display("FINAL STATUS: FAILED. %0d out of %0d UNSIGNED Combinational MultAcc tests did not pass.", num_errors, test_id_counter);end$display("==================================================");$finish;
endendmodule 

結(jié)果

window模塊每個(gè)周期傳遞數(shù)據(jù),因而采用組合邏輯實(shí)現(xiàn)卷積運(yùn)算。當(dāng)輸入數(shù)據(jù)同時(shí)有效,也就是window_valid和weight_valid同時(shí)為高時(shí),mult_acc_com進(jìn)行運(yùn)算,conv_valid拉高,如下圖所示

在這里插入圖片描述

輸出打印結(jié)果:

=Comprehensive UNSIGNED Combinational MultAcc Test (OUTPUT_WIDTH=20) =
Test Description: Simple Positive Values (1*1, sum 27)
Expected: conv_valid=1, conv_out= 27
Actual: conv_valid=1, conv_out= 27

Test ID 1: Status: PASSED

Test Description: Positive Values with Saturation (2*3, raw 162, sat 162)
Expected: conv_valid=1, conv_out= 162
Actual: conv_valid=1, conv_out= 162

Test ID 2: Status: PASSED

Test Description: Invalid Inputs (both valid_n low)
Expected: conv_valid=0, conv_out= 0
Actual: conv_valid=0, conv_out= 0

Test ID 3: Status: PASSED

Test Description: Zero Window Data, Non-zero Weights
Expected: conv_valid=1, conv_out= 0
Actual: conv_valid=1, conv_out= 0

Test ID 4: Status: PASSED

Test Description: Non-zero Window, Zero Weight Data
Expected: conv_valid=1, conv_out= 0
Actual: conv_valid=1, conv_out= 0

Test ID 5: Status: PASSED

Test Description: All Zero Inputs
Expected: conv_valid=1, conv_out= 0
Actual: conv_valid=1, conv_out= 0

Test ID 6: Status: PASSED

Test Description: Large values (no saturation with 20-bit output)
Expected: conv_valid=1, conv_out= 675
Actual: conv_valid=1, conv_out= 675

Test ID 7: Status: PASSED

Test Description: Max Val Inputs (Win= 255, Wgt= 255), should saturate to 1048575
Expected: conv_valid=1, conv_out=1048575
Actual: conv_valid=1, conv_out=1048575

Test ID 8: Status: PASSED

Test Description: Medium values to test 20-bit range (100*100, sum 270000)
Expected: conv_valid=1, conv_out= 270000
Actual: conv_valid=1, conv_out= 270000

Test ID 9: Status: PASSED

— Test Sequence 9: Window Valid Toggles (base inputs 1*1, sum 27) —
Sub-Test Description: WinValid=1 (Start)
Expected: conv_valid=1, conv_out= 27
Actual: conv_valid=1, conv_out= 27

Test ID 10: Status: PASSED

Sub-Test Description: WinValid=0
Expected: conv_valid=0, conv_out= 0
Actual: conv_valid=0, conv_out= 0

Test ID 11: Status: PASSED

Sub-Test Description: WinValid=1 (End)
Expected: conv_valid=1, conv_out= 27
Actual: conv_valid=1, conv_out= 27

Test ID 12: Status: PASSED

— Test Sequence 10: Weight Valid Toggles (base inputs 1*1, sum 27) —
Sub-Test Description: WeightValid=1 (Start)
Expected: conv_valid=1, conv_out= 27
Actual: conv_valid=1, conv_out= 27

Test ID 13: Status: PASSED

Sub-Test Description: WeightValid=0
Expected: conv_valid=0, conv_out= 0
Actual: conv_valid=0, conv_out= 0

Test ID 14: Status: PASSED

Sub-Test Description: WeightValid=1 (End)
Expected: conv_valid=1, conv_out= 27
Actual: conv_valid=1, conv_out= 27

Test ID 15: Status: PASSED

http://m.risenshineclean.com/news/60955.html

相關(guān)文章:

  • 高端網(wǎng)站建設(shè)深圳寧德市屬于哪個(gè)省份
  • 網(wǎng)站方案策劃怎么寫免費(fèi)網(wǎng)站推廣軟件下載
  • 怎么樣提高網(wǎng)站點(diǎn)擊率高明公司搜索seo
  • 上海城鄉(xiāng)建設(shè)網(wǎng)站如何制作一個(gè)屬于自己的網(wǎng)站
  • 接單做效果圖網(wǎng)站域名查詢?cè)L問(wèn)
  • 企業(yè)營(yíng)銷型網(wǎng)站做的好小程序推廣方案
  • 小企業(yè)網(wǎng)站建設(shè)的措施群站優(yōu)化之鏈輪模式
  • 網(wǎng)站開發(fā)流程詳細(xì)介紹日本櫻花免m38vcom費(fèi)vps
  • 網(wǎng)站后臺(tái)功能開發(fā)seo網(wǎng)站分析工具
  • 聯(lián)合實(shí)驗(yàn)室 網(wǎng)站建設(shè)方案seo網(wǎng)站地圖
  • 什么企業(yè)做網(wǎng)站網(wǎng)站點(diǎn)擊快速排名
  • wordpress上傳圖片x整站優(yōu)化關(guān)鍵詞推廣
  • 網(wǎng)站設(shè)計(jì)的思路網(wǎng)站建設(shè)有多少公司
  • php網(wǎng)站權(quán)限設(shè)置網(wǎng)站鏈接查詢
  • 自己做的網(wǎng)站鏈接到微信支付界面微信推廣方法
  • 如何卸載和重裝wordpress保定網(wǎng)站seo
  • 重慶平臺(tái)網(wǎng)站建設(shè)平臺(tái)推廣網(wǎng)站文案
  • 房產(chǎn)網(wǎng)站制作流程關(guān)鍵詞的作用
  • 軟件管理工程師福州seo優(yōu)化排名推廣
  • 成全視頻在線觀看在線播放seo是做什么工作的
  • 做國(guó)外的網(wǎng)站網(wǎng)絡(luò)推廣優(yōu)化服務(wù)
  • 網(wǎng)站icp備案咋做查詢網(wǎng)138網(wǎng)站域名
  • 網(wǎng)站o2o如何制作一個(gè)網(wǎng)址
  • 兩個(gè)相同的網(wǎng)站對(duì)做優(yōu)化有幫助網(wǎng)絡(luò)推廣渠道
  • 個(gè)人網(wǎng)站設(shè)計(jì)內(nèi)容和要求百度圖片搜索引擎入口
  • 廣西網(wǎng)站建設(shè)哪家好關(guān)鍵字
  • 網(wǎng)站icp備案怎么做全網(wǎng)營(yíng)銷與seo
  • 天津網(wǎng)站建設(shè)哪家有百度市場(chǎng)應(yīng)用官方app
  • 如何提升網(wǎng)站速度網(wǎng)絡(luò)營(yíng)銷模式有哪些?
  • 做調(diào)查賺錢網(wǎng)站推廣營(yíng)銷軟件app