2020华为软件精英挑战赛热身赛总结

在这里插入图片描述


Hello大家好,这里是西北赛区“让心跳动次动次”,我是队长SUN,先说一下成绩吧,热身赛个人排名56,最后两天从20+掉到60+,官方查重也没把我救回前50。初赛西北赛区第四,复赛A榜西北赛区第五,B榜5连WA(0%)。据我所知,西北赛区复赛A榜前8就chier大佬成功晋级决赛,吐槽这里就不写了,大佬牛批就完了。

热身赛一路走来,两个多月几乎每天都在认真做比赛,不谈最终结果,收获也是蛮多。日常在Family和LPL群里潜水听各位大佬授课,能结识一些志同道合的小伙伴,这就够了,20年软挑再次折戟,明年作为一只研三狗不知道还有没有时间继续参加。

回归正文,这里是热身赛总结,热身赛最后崩盘的主要原因是没有使用仅读取部分数据进行预测的trick,导致IO时间过长,不过这并不是重点,以下将分享我在热身赛中的一些其他优化,初赛和复赛总结将会在下一篇!

00 赛题描述

热身赛赛题是一个二分类问题,官方已经做好了特征工程处理,并给出了逻辑回归的baseline,需要选手结合对机器学习算法的理解并结合鲲鹏处理器的特点(如:多核、NEON,Cache大小)对其进行优化,准确率高于70%开始计分。

01 整体思路

赛题大体上分为4个部分,数据读取与转换,模型训练,模型预测和生成结果文件。

  • 数据读取与转换
  1. 通过mmap对训练文件进行映射
  2. 利用四线程对读取的字符进行处理,转为float存储
  • 模型训练
  1. 使用Mini-Batch Gradient Descent (MBGD) 进行梯度下降
  2. 利用NEON对矩阵运算进行加速
  • 模型预测
  1. 通过mmap对测试文件进行映射
  2. 利用多线程解析字符同时进行预测
  3. 多线程中使用NEON解析字符转为float
  • 生成结果文件
  1. 使用fprintf将预测结果写入

02 数据读取与转换

数据读取部分,主线程通过mmap获取数据指针和所有字符数,对所有字符进行4等分,调整使得4个线程从行头开始解析。

inline bool LR::loadTrainData(){
    char *buf = NULL;
    //获取文件描述符
    int fd = open(trainFile.c_str(),O_RDONLY);
    if(fd < 0) {
        cout << "打开文件失败" << endl;
        return false;
    }
    //得到大于901行的字符数,每行字符小于6200。实际训练只用了901行训练数据。
    long filelen = 6200 * 901;
    buf = (char *) mmap(NULL, filelen, PROT_READ, MAP_PRIVATE, fd, 0);
    //进行分割,均分为4份
    int splitNum= filelen / 4 + 1;
    int start2 = splitNum, start3 = 2 * splitNum, start4 = 3 * splitNum, start5 = filelen;
    //需要对开始结束进行调整,便于进行存储
    while(buf[start2] != '\n') ++start2;
    while(buf[start3] != '\n') ++start3;
    while(buf[start4] != '\n') ++start4;
	while(buf[start5] != '\n') --start5;
    //开启四个线程
    thread  th1(MultiSplitTrain, buf, 0, start2, trainDataSet1, &trainNum1);
    thread  th2(MultiSplitTrain, buf, start2 + 1, start3, trainDataSet2, &trainNum2);
    thread  th3(MultiSplitTrain, buf, start3 + 1, start4, trainDataSet3, &trainNum3);
    thread  th4(MultiSplitTrain, buf, start4 + 1, start5, trainDataSet4, &trainNum4);
    th1.join();
    th2.join();
    th3.join();
    th4.join();
    //解除映射
	munmap(buf, filelen);
    return true;
}

数据转换部分,自己写的转换函数。

inline void MultiSplitTrain(char* buf, int start, int end, float* data, int* Num){
    int num = 0;
    int n = start;
    while(n < end){
        if(buf[n] == ','){
            ++n;
            continue;
        }
        if(buf[n] == '\n'){
            ++n;
            continue;
        }
        int pos = 0;
        int flag = 1;
        float res = 0.0;
        if(buf[n] == '-'){
             flag = -1;
             n++;
         }
        //此处利用查表避免乘法运算
        res += Mul2Add[buf[n++] - '0'][pos];
        if('.' == buf[n++]){
             ++pos;
            while(buf[n] >= '0' && buf[n] <= '9'){
                if(pos < 6) res += Mul2Add[buf[n++] - '0'][pos++];
            }
         }
        *(data + num++) = flag * res;
    }
    //记录行数
    *Num = num / 1001;
}

此处的字符转换函数必须自己写,库函数速度相当慢,此外,在进行字符转化时,利用查表得到浮点数避免乘法运算,表如下:

//查表法 线上数据格式为x.xxx 所以到0.00x即可
float Mul2Add[10][4] = {
    {0, 0, 0, 0},
    {1, 0.1, 0.01, 0.001},
    {2, 0.2, 0.02, 0.002},
    {3, 0.3, 0.03, 0.003},
    {4, 0.4, 0.04, 0.004},
    {5, 0.5, 0.05, 0.005},
    {6, 0.6, 0.06, 0.006},
    {7, 0.7, 0.07, 0.007},
    {8, 0.8, 0.08, 0.008},
    {9, 0.9, 0.09, 0.009},
};

优化点:
1. mmap读取
2. 多线程转换
3. 自写atof函数
4. 查表

03 模型训练

单线程,主要利用了NEON对矩阵运算进行加速。

inline void LR::train()
{
	//临时权重表
    float  WtSet[featuresNum];
    memset(WtSet, 0, sizeof(WtSet));
    float32x4_t traindata_vec;
    float32x4_t wtdata_vec;
    float32x4_t feat_vec;
    float32x4_t WtSet_vec;
    float32x4_t espvInv_vec;
    float32x4_t WtFeature_vec;
    float32x4_t Wtset_vec;
    float32x4_t MulWtSetBatch_vec;
    float32x4_t stepSize_vec;
    float32x4_t batch_vec;
    float32x4_t mul_vec;
	float* trainDataSet;
	//进行迭代
    for (int i = 0; i < maxIterTimes; i++) {
        int start = random(trainNum - batch); //起始位置
        for(int i = start; i < start + batch; ++i){
			//确定内存位置
			if(i < trainNum1) trainDataSet = trainDataSet1 + i * (featuresNum + 1);
			else if(i < trainNum1 + trainNum2) trainDataSet = trainDataSet2 + (i - trainNum1) * (featuresNum + 1);
			else if(i < trainNum1 + trainNum2 + trainNum3) trainDataSet = trainDataSet3 + (i - trainNum1 - trainNum2) * (featuresNum + 1);
			else  trainDataSet = trainDataSet4 + (i - trainNum1 - trainNum2 - trainNum3) * (featuresNum + 1);

			mul_vec = vdupq_n_f32(0.0);
			for(int j = 0; j < featuresNum; j += 4){
				traindata_vec = vld1q_f32(trainDataSet + j);
				wtdata_vec = vld1q_f32(WtFeature + j);
				mul_vec = vmlaq_f32(mul_vec, traindata_vec, wtdata_vec);
			}
			float  mulSum = vgetq_lane_f32(mul_vec, 0)+vgetq_lane_f32(mul_vec, 1)+vgetq_lane_f32(mul_vec, 2)+vgetq_lane_f32(mul_vec, 3);
			float  expvInv = (*(trainDataSet + featuresNum)) - sigmoidCalc(mulSum);
			espvInv_vec = vdupq_n_f32(expvInv);
			for(int j = 0; j < featuresNum; j += 4){
				feat_vec = vld1q_f32(trainDataSet + j);
				WtSet_vec = vld1q_f32(WtSet + j);
				WtSet_vec = vmlaq_f32(WtSet_vec, feat_vec, espvInv_vec);
				vst1q_f32(WtSet + j, WtSet_vec);
			}
		}
		stepSize_vec = vdupq_n_f32(stepSize);
		float vbatch = (float)1 / (float)batch;
		batch_vec = vdupq_n_f32(vbatch);
		for(int j = 0; j < featuresNum; j += 4){
			WtFeature_vec = vld1q_f32(WtFeature + j);
			Wtset_vec = vld1q_f32(WtSet + j);
			MulWtSetBatch_vec = vmulq_f32(Wtset_vec, batch_vec);
			WtFeature_vec = vmlaq_f32(WtFeature_vec, stepSize_vec, MulWtSetBatch_vec);
			vst1q_f32(WtFeature + j, WtFeature_vec);
		}
		memset(WtSet, 0, sizeof(WtSet));
	}
}

这部分代码因为加入了大量的NEON运算所以比较难读,但只要搞懂了NEON的用法看起来就简单多了,本质是简单的逻辑回归梯度下降。

优化点:
1. NEON加速矩阵运算
2. 减少训练集的个数,极大地减少读IO时间,减小batch大小和迭代次数,降低训练时间。我的参数为训练样本901,学习率0.024,最大迭代次数500,batch为2,初始权1.2。参数是通过线上测试出来的,卡69.5%正确率。

04 模型预测

测试数据读取部分和训练数据读取类似,不过采用了8线程进行了解析预测,虽然线上评测机只有4核,但经测试8线程比4线程快,考虑可能是负载不均衡导致的。在这里贴一个NEON解析字符串的魔法,不是自己想出来的,是大佬在群里分享的,觉得很有意思。

#include <iostream>
using namespace std;
#include <arm_neon.h>
#include <bits/stdc++.h>

const char T[16] = {0,10,0,10,0,10,0,10,0,10,0,10,0,10,0,10};
const char T2[16] = {100,1,100,1,100,1,100,1,100,1,100,1,100,1,100,1};
const char S1[16] = {64,240,64,240,64,240,64,240,64,240,64,240,64,240,64,240};

char buf[200] = "\n0.245,0.467,1.587,0.456,0.444,0.128,0.111,0.101,0.445,\n";

short read_result[1010];
const int N = 8;
void read(char *pc){
    uint8x16x3_t cval;
    int16x8_t ca;
    uint8x16_t M1 = vld1q_u8((const uint8_t*)T),M2 = vld1q_u8((const uint8_t*)T2),M3 = vld1q_u8((const uint8_t*)S1);
    for(int i=0;i<N;i+=8){
        cval = vld3q_u8((const uint8_t*)pc);
        ca = vreinterpretq_s16_u16(vpaddlq_u8(vaddq_u8(M3, vaddq_u8(vmulq_u8(cval.val[0],M1) , vmulq_u8(cval.val[1],M2)))));
        vst1q_s16(read_result+i,ca);
        pc+=48;
    }
}
int main()
{
    read(buf);
	for(int i = 0; i < 20; ++i) cout << read_result[i] << endl;
    return 0;
}

通过这种方法可以得到所需数据,但因为线上时间的主要瓶颈在IO,所以对成绩影响不大。不过,我相信看懂了这个,基本也就熟悉了NEON的用法,我对测试集的解析也使用了这种思路。

const char T[8] = {10,10,10,10,10,10,10,10};
const char T2[8] = {0,1,0,1,0,1,0,1};
const char T3[8] = {1,0,1,0,1,0,1,0};
const char S2[8] = {100,1,100,1,100,1,100,1};
inline void MultiSplitTest(char* buf, int start, int row, float* WtFeature, int* predict){
	float testData[4];
    uint8x8x3_t cval;
    int32x4_t ca;
	float32x4_t mul_vec;
	float32x4_t wtdata_vec;
	float32x4_t testdata_vec;
    uint8x8_t hund_vec = vdup_n_u8(100);
    uint8x8_t M1 = vld1_u8((const uint8_t*)T),M2 = vld1_u8((const uint8_t*)T2),M3 = vld1_u8((const uint8_t*)T3),M4 = vdup_n_u8(240),M5 = vld1_u8((const uint8_t*)S2);
	int N = 1000;
	int n = start;
	for(int i = 0; i < row; ++i){
		mul_vec = vdupq_n_f32(0.0);
		for(int j = 0; j < N; j += 4){
			cval = vld3_u8((const uint8_t*)(buf + n));
			ca = vreinterpretq_s32_u32(vpaddlq_u16(vmull_u8(vadd_u8(M4, vadd_u8(vadd_u8(vmul_u8(cval.val[0],M1) , vmul_u8(cval.val[1],M2)), vmul_u8(cval.val[2],M3))), M5)));
			testData[0] = int2Float[vgetq_lane_s32(ca, 0)];
			testData[1] = int2Float[vgetq_lane_s32(ca, 1)];
			testData[2] = int2Float[vgetq_lane_s32(ca, 2)];
			testData[3] = int2Float[vgetq_lane_s32(ca, 3)];
			testdata_vec = vld1q_f32(testData);
			wtdata_vec = vld1q_f32(WtFeature + j);
			mul_vec = vmlaq_f32(mul_vec, testdata_vec, wtdata_vec);
			n += 24;
		}
		float  mulSum = vgetq_lane_f32(mul_vec, 0)+vgetq_lane_f32(mul_vec, 1)+vgetq_lane_f32(mul_vec, 2)+vgetq_lane_f32(mul_vec, 3);
		*(predict + i) = mulSum >= 0 ? 1 : 0;
	}
}

优化点:
1. mmap映射读文件
2. 读取不必全读3位小数
3. 多线程解析
4. 边读边预测
5. NEON在解析中的用法

05 生成结果文件

inline int LR::storePredict()
{
    FILE* f;
    f = fopen(predictOutFile.c_str(), "w");
    if(NULL == f){
        cout << "storePredict error" << endl;
        return false;
    }
    for (int i = 0; i < testNum; i++) {
        fprintf(f, "%d\n", *(predictVec + i));
    }
    fclose(f);
    return 0;
}

最后,开源请点击这里,经过初赛复赛的训练,发现这份代码还有一些点可以优化,比如取消类、多线程训练、全局静态数组等等,但更重要的还是IO时间,大佬们基本都用了仅读取第一维特征去预测,减少了大量的读测试数据IO时间。针对鲲鹏处理器的优化,我利用了多核和NEON,至于针对Cache的优化,自己也是一知半解,部分思路将会在初赛分享中给出,敬请期待~

补充,学习了大佬们的开源,整理一下:

大佬们的trick合集:

1. 可仅通过第一维对数据进行预测
2. 数字可以只读小数点后一位,忽略其他所有数位
3. 测试集中没有负号,推测训练集中带符号的数据没有用,直接忽略
4. switch-case比if-else要快一些
5. memcpy比字符数组挨个赋值要快
6. 预测时,多进程比多线程快
7. 输出到文件中的换行符用endl会超级慢

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值